From aaba01d534290213b2f37cb28b2a1a48f204a074 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 21 Jul 2012 09:38:12 +0530 Subject: [PATCH 01/48] Book details panel: ALlow right clicking on a format to delete it. --- src/calibre/gui2/actions/delete.py | 15 +++++++++++ src/calibre/gui2/book_details.py | 43 ++++++++++++++++++++++++++++-- src/calibre/gui2/init.py | 2 ++ 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index 161a4788c2..135591aa10 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -139,6 +139,21 @@ class DeleteAction(InterfaceAction): return set([]) return set(map(self.gui.library_view.model().id, rows)) + def remove_format_by_id(self, book_id, fmt): + title = self.gui.current_db.title(book_id, index_is_id=True) + if not confirm('

'+(_( + 'The %(fmt)s format will be permanently deleted from ' + '%(title)s. Are you sure?')%dict(fmt=fmt, title=title)) + +'

', 'library_delete_specific_format', self.gui): + return + + self.gui.library_view.model().db.remove_format(book_id, fmt, + index_is_id=True, notify=False) + self.gui.library_view.model().refresh_ids([book_id]) + self.gui.library_view.model().current_changed(self.gui.library_view.currentIndex(), + self.gui.library_view.currentIndex()) + self.gui.tags_view.recount() + def delete_selected_formats(self, *args): ids = self._get_selected_ids() if not ids: diff --git a/src/calibre/gui2/book_details.py b/src/calibre/gui2/book_details.py index a8e5e20a70..acf5a8927f 100644 --- a/src/calibre/gui2/book_details.py +++ b/src/calibre/gui2/book_details.py @@ -5,8 +5,8 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import (QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, - QPropertyAnimation, QEasingCurve, QApplication, QFontInfo, +from PyQt4.Qt import (QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, QIcon, + QPropertyAnimation, QEasingCurve, QApplication, QFontInfo, QAction, QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette, QMenu) from PyQt4.QtWebKit import QWebView @@ -382,6 +382,7 @@ class CoverView(QWidget): # {{{ class BookInfo(QWebView): link_clicked = pyqtSignal(object) + remove_format = pyqtSignal(int, object) def __init__(self, vertical, parent=None): QWebView.__init__(self, parent) @@ -395,6 +396,16 @@ class BookInfo(QWebView): palette.setBrush(QPalette.Base, Qt.transparent) self.page().setPalette(palette) self.css = P('templates/book_details.css', data=True).decode('utf-8') + self.remove_format_action = QAction(QIcon(I('trash.png')), + '', self) + self.remove_format_action.current_fmt = None + self.remove_format_action.triggered.connect(self.remove_format_triggerred) + + def remove_format_triggerred(self): + f = self.remove_format_action.current_fmt + if f: + book_id, fmt = f + self.remove_format.emit(book_id, fmt) def link_activated(self, link): self._link_clicked = True @@ -420,6 +431,32 @@ class BookInfo(QWebView): else: ev.ignore() + def contextMenuEvent(self, ev): + p = self.page() + mf = p.mainFrame() + r = mf.hitTestContent(ev.pos()) + url = unicode(r.linkUrl().toString()).strip() + menu = p.createStandardContextMenu() + ca = self.pageAction(p.Copy) + for action in list(menu.actions()): + if action is not ca: + menu.removeAction(action) + if not r.isNull() and url.startswith('format:'): + parts = url.split(':') + try: + book_id, fmt = int(parts[1]), parts[2] + except: + import traceback + traceback.print_exc() + else: + self.remove_format_action.current_fmt = (book_id, fmt) + self.remove_format_action.setText(_('Delete the %s format')%parts[ + 2]) + menu.addAction(self.remove_format_action) + if len(menu.actions()) > 0: + menu.exec_(ev.globalPos()) + + # }}} class DetailsLayout(QLayout): # {{{ @@ -513,6 +550,7 @@ class BookDetails(QWidget): # {{{ show_book_info = pyqtSignal() open_containing_folder = pyqtSignal(int) view_specific_format = pyqtSignal(int, object) + remove_specific_format = pyqtSignal(int, object) remote_file_dropped = pyqtSignal(object, object) files_dropped = pyqtSignal(object, object) cover_changed = pyqtSignal(object, object) @@ -579,6 +617,7 @@ class BookDetails(QWidget): # {{{ self.book_info = BookInfo(vertical, self) self._layout.addWidget(self.book_info) self.book_info.link_clicked.connect(self.handle_click) + self.book_info.remove_format.connect(self.remove_specific_format) self.setCursor(Qt.PointingHandCursor) def handle_click(self, link): diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index d6332d71ac..9b63cd5ed8 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -265,6 +265,8 @@ class LayoutMixin(object): # {{{ type=Qt.QueuedConnection) self.book_details.open_containing_folder.connect(self.iactions['View'].view_folder_for_id) self.book_details.view_specific_format.connect(self.iactions['View'].view_format_by_id) + self.book_details.remove_specific_format.connect( + self.iactions['Remove Books'].remove_format_by_id) m = self.library_view.model() if m.rowCount(None) > 0: From 7d197152183e56ba1813cb022760741c02b4a933 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 21 Jul 2012 09:55:09 +0530 Subject: [PATCH 02/48] ... --- src/calibre/gui2/dialogs/confirm_delete.ui | 69 +++++++++++++--------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/src/calibre/gui2/dialogs/confirm_delete.ui b/src/calibre/gui2/dialogs/confirm_delete.ui index c45b180483..e5d35410db 100644 --- a/src/calibre/gui2/dialogs/confirm_delete.ui +++ b/src/calibre/gui2/dialogs/confirm_delete.ui @@ -1,7 +1,8 @@ - + + Dialog - - + + 0 0 @@ -9,51 +10,63 @@ 300 - + Are you sure? - - + + :/images/dialog_warning.png:/images/dialog_warning.png - - - + + + - - - :/images/dialog_warning.png + + + :/images/dialog_warning.png - - + + + + 0 + 0 + + + + + 300 + 0 + + + TextLabel - + true - - - + + + &Show this warning again - + true - - - + + + Qt::Horizontal - + QDialogButtonBox::Cancel|QDialogButtonBox::Ok @@ -61,7 +74,7 @@ - + @@ -70,11 +83,11 @@ Dialog accept() - + 248 254 - + 157 274 @@ -86,11 +99,11 @@ Dialog reject() - + 316 260 - + 286 274 From 7d1cf168d50ecca00a0b523a6a57f24c38727bd7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 21 Jul 2012 21:28:59 +0530 Subject: [PATCH 03/48] Fix Smashing Magazine --- recipes/smashing.recipe | 68 +++++++++++++---------------------------- 1 file changed, 21 insertions(+), 47 deletions(-) diff --git a/recipes/smashing.recipe b/recipes/smashing.recipe index 04436a05ef..a715ace821 100644 --- a/recipes/smashing.recipe +++ b/recipes/smashing.recipe @@ -1,50 +1,24 @@ -#!/usr/bin/env python - -__license__ = 'GPL v3' -__copyright__ = '2009, Darko Miletic ' -''' -www.smashingmagazine.com -''' - +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from calibre.web.feeds.news import BasicNewsRecipe -class SmashingMagazine(BasicNewsRecipe): - title = 'Smashing Magazine' - __author__ = 'Darko Miletic' - description = 'We smash you with the information that will make your life easier, really' - oldest_article = 20 - language = 'en' - max_articles_per_feed = 100 - no_stylesheets = True - use_embedded_content = False - publisher = 'Smashing Magazine' - category = 'news, web, IT, css, javascript, html' - encoding = 'utf-8' +class SmashingMagazine (BasicNewsRecipe): + __author__ = 'Marc Busqué ' + __url__ = 'http://www.lamarciana.com' + __version__ = '1.0.1' + __license__ = 'GPL v3' + __copyright__ = '2012, Marc Busqué ' + title = u'Smashing Magazine' + description = u'Founded in September 2006, Smashing Magazine delivers useful and innovative information to Web designers and developers. Our aim is to inform our readers about the latest trends and techniques in Web development. We try to persuade you not with the quantity but with the quality of the information we present. Smashing Magazine is and always has been independent.' + language = 'en' + tags = 'web development, software' + oldest_article = 7 + remove_empty_feeds = True + no_stylesheets = True + encoding = 'utf8' + cover_url = u'http://media.smashingmagazine.com/themes/smashingv4/images/logo.png' + remove_attributes = ['border', 'cellspacing', 'align', 'cellpadding', 'colspan', 'valign', 'vspace', 'hspace', 'alt', 'width', 'height', 'style'] + extra_css = u'body div table:first-child {display: none;} img {max-width: 100%; display: block; margin: auto;}' - conversion_options = { - 'comments' : description - ,'tags' : category - ,'publisher' : publisher - } - - keep_only_tags = [dict(name='div', attrs={'id':'leftcolumn'})] - remove_tags_after = dict(name='ul',attrs={'class':'social'}) - remove_tags = [ - dict(name=['link','object']) - ,dict(name='h1',attrs={'class':'logo'}) - ,dict(name='div',attrs={'id':'booklogosec'}) - ,dict(attrs={'src':'http://media2.smashingmagazine.com/wp-content/uploads/images/the-smashing-book/smbook6.gif'}) - ] - - feeds = [(u'Articles', u'http://rss1.smashingmagazine.com/feed/')] - - def preprocess_html(self, soup): - for iter in soup.findAll('div',attrs={'class':'leftframe'}): - it = iter.find('h1') - if it == None: - iter.extract() - for item in soup.findAll('img'): - oldParent = item.parent - if oldParent.name == 'a': - oldParent.name = 'div' - return soup + feeds = [ + (u'Smashing Magazine', u'http://rss1.smashingmagazine.com/feed/'), + ] From ebfce709796ca5406749c26a47b11bd7a61613f4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 21 Jul 2012 21:47:27 +0530 Subject: [PATCH 04/48] A list apart and .net magazine by Marc Busque --- recipes/dot_net.recipe | 32 ++++++++++++++++++++++++++++++++ recipes/list_apart.recipe | 33 +++++++++++++++++++++++++++++++++ recipes/smashing.recipe | 4 ++-- 3 files changed, 67 insertions(+), 2 deletions(-) create mode 100644 recipes/dot_net.recipe create mode 100644 recipes/list_apart.recipe diff --git a/recipes/dot_net.recipe b/recipes/dot_net.recipe new file mode 100644 index 0000000000..50db71e9be --- /dev/null +++ b/recipes/dot_net.recipe @@ -0,0 +1,32 @@ +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from calibre.web.feeds.news import BasicNewsRecipe +import re + +class NetMagazineRecipe (BasicNewsRecipe): + __author__ = u'Marc Busqué ' + __url__ = 'http://www.lamarciana.com' + __version__ = '1.0' + __license__ = 'GPL v3' + __copyright__ = u'2012, Marc Busqué ' + title = u'.net magazine' + description = u'net is the world’s best-selling magazine for web designers and developers, featuring tutorials from leading agencies, interviews with the web’s biggest names, and agenda-setting features on the hottest issues affecting the internet today.' + language = 'en' + tags = 'web development, software' + oldest_article = 7 + remove_empty_feeds = True + no_stylesheets = True + cover_url = u'http://media.netmagazine.futurecdn.net/sites/all/themes/netmag/logo.png' + keep_only_tags = [ + dict(name='article', attrs={'class': re.compile('^node.*$', re.IGNORECASE)}) + ] + remove_tags = [ + dict(name='span', attrs={'class': 'comment-count'}), + dict(name='div', attrs={'class': 'item-list share-links'}), + dict(name='footer'), + ] + remove_attributes = ['border', 'cellspacing', 'align', 'cellpadding', 'colspan', 'valign', 'vspace', 'hspace', 'alt', 'width', 'height', 'style'] + extra_css = 'img {max-width: 100%; display: block; margin: auto;} .captioned-image div {text-align: center; font-style: italic;}' + + feeds = [ + (u'.net', u'http://feeds.feedburner.com/net/topstories'), + ] diff --git a/recipes/list_apart.recipe b/recipes/list_apart.recipe new file mode 100644 index 0000000000..35cbaad958 --- /dev/null +++ b/recipes/list_apart.recipe @@ -0,0 +1,33 @@ +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from calibre.web.feeds.news import BasicNewsRecipe + +class AListApart (BasicNewsRecipe): + __author__ = u'Marc Busqué ' + __url__ = 'http://www.lamarciana.com' + __version__ = '1.0' + __license__ = 'GPL v3' + __copyright__ = u'2012, Marc Busqué ' + title = u'A List Apart' + description = u'A List Apart Magazine (ISSN: 1534-0295) explores the design, development, and meaning of web content, with a special focus on web standards and best practices.' + language = 'en' + tags = 'web development, software' + oldest_article = 120 + remove_empty_feeds = True + no_stylesheets = True + encoding = 'utf8' + cover_url = u'http://alistapart.com/pix/alalogo.gif' + keep_only_tags = [ + dict(name='div', attrs={'id': 'content'}) + ] + remove_tags = [ + dict(name='ul', attrs={'id': 'metastuff'}), + dict(name='div', attrs={'class': 'discuss'}), + dict(name='div', attrs={'class': 'discuss'}), + dict(name='div', attrs={'id': 'learnmore'}), + ] + remove_attributes = ['border', 'cellspacing', 'align', 'cellpadding', 'colspan', 'valign', 'vspace', 'hspace', 'alt', 'width', 'height'] + extra_css = u'img {max-width: 100%; display: block; margin: auto;} #authorbio img {float: left; margin-right: 2%;}' + + feeds = [ + (u'A List Apart', u'http://www.alistapart.com/site/rss'), + ] diff --git a/recipes/smashing.recipe b/recipes/smashing.recipe index a715ace821..bc24166275 100644 --- a/recipes/smashing.recipe +++ b/recipes/smashing.recipe @@ -2,11 +2,11 @@ from calibre.web.feeds.news import BasicNewsRecipe class SmashingMagazine (BasicNewsRecipe): - __author__ = 'Marc Busqué ' + __author__ = u'Marc Busqué ' __url__ = 'http://www.lamarciana.com' __version__ = '1.0.1' __license__ = 'GPL v3' - __copyright__ = '2012, Marc Busqué ' + __copyright__ = u'2012, Marc Busqué ' title = u'Smashing Magazine' description = u'Founded in September 2006, Smashing Magazine delivers useful and innovative information to Web designers and developers. Our aim is to inform our readers about the latest trends and techniques in Web development. We try to persuade you not with the quantity but with the quality of the information we present. Smashing Magazine is and always has been independent.' language = 'en' From cc61cca75aba84ee7724d67247d8e2da7eb94e38 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 22 Jul 2012 00:12:30 +0530 Subject: [PATCH 05/48] Fix #1027431 (Cannot Connect Samsung Skyrocket S2 (Android 4.04 OS)) --- src/calibre/devices/android/driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index f17eba522b..eca5d615ce 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -212,7 +212,7 @@ class ANDROID(USBMS): 'KTABLET_PC', 'INGENIC', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX', 'THINKPAD_TABLET', 'SGH-T989', 'YP-G70', 'STORAGE_DEVICE', - 'ADVANCED'] + 'ADVANCED', 'SGH-I727'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD', @@ -221,7 +221,7 @@ class ANDROID(USBMS): 'A1-07___C0541A4F', 'XT912', 'MB855', 'XT910', 'BOOK_A10_CARD', 'USB_2.0_DRIVER', 'I9100T', 'P999DW_SD_CARD', 'KTABLET_PC', 'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'XT875', - 'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD'] + 'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727'] OSX_MAIN_MEM = 'Android Device Main Memory' From 73a40c78a10ba0b0126abae94e386ef59a68ec26 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 22 Jul 2012 09:59:20 +0530 Subject: [PATCH 06/48] Improve Anadtech --- recipes/anandtech.recipe | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/recipes/anandtech.recipe b/recipes/anandtech.recipe index aa10084070..ff08c828ac 100644 --- a/recipes/anandtech.recipe +++ b/recipes/anandtech.recipe @@ -21,8 +21,12 @@ class anan(BasicNewsRecipe): remove_javascript = True encoding = 'utf-8' - remove_tags=[dict(name='a', attrs={'style':'width:110px; margin-top:0px;text-align:center;'}), - dict(name='a', attrs={'style':'width:110px; margin-top:0px; margin-right:20px;text-align:center;'})] + remove_tags=[ + dict(name='a', attrs={'style':'width:110px; margin-top:0px;text-align:center;'}), + dict(name='a', attrs={'style':'width:110px; margin-top:0px; margin-right:20px;text-align:center;'}), + {'attrs':{'class':['article_links', 'header', 'body_right']}}, + {'id':['crumbs']}, + ] feeds = [ ('Anandtech', 'http://www.anandtech.com/rss/')] From 9015213161485fc417dfd78b22d9eefb971bf2db Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 23 Jul 2012 15:09:43 +0530 Subject: [PATCH 07/48] Fix San Francisco Bay Guardian --- recipes/sfbg.recipe | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/recipes/sfbg.recipe b/recipes/sfbg.recipe index 0735e760c6..5c77c96f74 100644 --- a/recipes/sfbg.recipe +++ b/recipes/sfbg.recipe @@ -1,25 +1,35 @@ from calibre.web.feeds.news import BasicNewsRecipe class SanFranciscoBayGuardian(BasicNewsRecipe): - title = u'San Francisco Bay Guardian' - language = 'en' - __author__ = 'Krittika Goyal' + title = u'San Francisco Bay Guardian' + language = 'en' + __author__ = 'Krittika Goyal' oldest_article = 31 #days max_articles_per_feed = 25 + #encoding = 'latin1' no_stylesheets = True + #remove_tags_before = dict(name='div', attrs={'id':'story_header'}) + #remove_tags_after = dict(name='div', attrs={'id':'shirttail'}) remove_tags = [ - dict(name='iframe'), + dict(name='iframe'), + #dict(name='div', attrs={'class':'related-articles'}), + #dict(name='div', attrs={'id':['story_tools', 'toolbox', 'shirttail', 'comment_widget']}), + #dict(name='ul', attrs={'class':'article-tools'}), + #dict(name='ul', attrs={'id':'story_tabs'}), ] feeds = [ ('sfbg', 'http://www.sfbg.com/rss.xml'), - ('politics', 'http://www.sfbg.com/politics/rss.xml'), - ('blogs', 'http://www.sfbg.com/blog/rss.xml'), - ('pixel_vision', 'http://www.sfbg.com/pixel_vision/rss.xml'), - ('bruce', 'http://www.sfbg.com/bruce/rss.xml'), ] - + #def preprocess_html(self, soup): + #story = soup.find(name='div', attrs={'id':'story_body'}) + #td = heading.findParent(name='td') + #td.extract() + #soup = BeautifulSoup('t') + #body = soup.find(name='body') + #body.insert(0, story) + #return soup From 65103e0ac12ab0e58943f261ba0809204678d22c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 23 Jul 2012 21:31:18 +0530 Subject: [PATCH 08/48] ... --- src/calibre/devices/prst1/driver.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/calibre/devices/prst1/driver.py b/src/calibre/devices/prst1/driver.py index a2b3716c05..92db8c6142 100644 --- a/src/calibre/devices/prst1/driver.py +++ b/src/calibre/devices/prst1/driver.py @@ -280,17 +280,17 @@ class PRST1(USBMS): try: cursor = connection.cursor() - + debug_print("Removing Orphaned Collection Records") - + # Purge any collections references that point into the abyss query = 'DELETE FROM collections WHERE content_id NOT IN (SELECT _id FROM books)' cursor.execute(query) query = 'DELETE FROM collections WHERE collection_id NOT IN (SELECT _id FROM collection)' cursor.execute(query) - + debug_print("Removing Orphaned Book Records") - + # Purge any references to books not in this database # Idea is to prevent any spill-over where these wind up applying to some other book query = 'DELETE FROM %s WHERE content_id NOT IN (SELECT _id FROM books)' @@ -301,7 +301,7 @@ class PRST1(USBMS): cursor.execute(query%'history') cursor.execute(query%'layout_cache') cursor.execute(query%'preference') - + cursor.close() except DatabaseError: import traceback @@ -320,7 +320,7 @@ class PRST1(USBMS): query = 'SELECT last_insert_rowid()' cursor.execute(query) row = cursor.fetchone() - + return long(row[0]) def get_database_min_id(self, source_id): @@ -376,6 +376,8 @@ class PRST1(USBMS): # Record what the max id being used is as well. db_books = {} for i, row in enumerate(cursor): + if row is None: + continue lpath = row[0].replace('\\', '/') db_books[lpath] = row[1] if row[1] < sequence_min: From f0d8a5cf1c3a3b695df53127b199755199ea7ce7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 23 Jul 2012 21:35:12 +0530 Subject: [PATCH 09/48] ... --- src/calibre/devices/prst1/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/prst1/driver.py b/src/calibre/devices/prst1/driver.py index 92db8c6142..d3c92b5eff 100644 --- a/src/calibre/devices/prst1/driver.py +++ b/src/calibre/devices/prst1/driver.py @@ -376,7 +376,7 @@ class PRST1(USBMS): # Record what the max id being used is as well. db_books = {} for i, row in enumerate(cursor): - if row is None: + if row[0] is None: continue lpath = row[0].replace('\\', '/') db_books[lpath] = row[1] From 5a854040e7721b405c82c517c2ee11748666ee03 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Mon, 23 Jul 2012 18:55:02 +0200 Subject: [PATCH 10/48] Base changes required to add the smart device driver --- src/calibre/devices/interface.py | 18 +++++++++ .../ebooks/metadata/book/json_codec.py | 36 +++++++++++------- src/calibre/gui2/device.py | 38 ++++++++++++++++--- .../gui2/device_drivers/configwidget.py | 3 ++ .../gui2/device_drivers/configwidget.ui | 13 +++++++ src/calibre/library/server/base.py | 7 +++- src/calibre/utils/Zeroconf.py | 5 +++ src/calibre/utils/mdns.py | 27 +++++++++++++ 8 files changed, 126 insertions(+), 21 deletions(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 9510dcf3d1..26239b59e7 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -15,6 +15,8 @@ class DevicePlugin(Plugin): #: Ordered list of supported formats FORMATS = ["lrf", "rtf", "pdf", "txt"] + # If True, the config dialog will not show the formats box + HIDE_FORMATS_CONFIG_BOX = False #: VENDOR_ID can be either an integer, a list of integers or a dictionary #: If it is a dictionary, it must be a dictionary of dictionaries, @@ -496,6 +498,22 @@ class DevicePlugin(Plugin): ''' return paths + def startup(self): + ''' + Called when calibre is is starting the device. Do any initialization + required. Note that multiple instances of the class can be instantiated, + and thus __init__ can be called multiple times, but only one instance + will have this method called. + ''' + pass + + def shutdown(self): + ''' + Called when calibre is shutting down, either for good or in preparation + to restart. Do any cleanup required. + ''' + pass + class BookList(list): ''' A list of books. Each Book object must have the fields diff --git a/src/calibre/ebooks/metadata/book/json_codec.py b/src/calibre/ebooks/metadata/book/json_codec.py index 3b52821c1b..8e2dc64383 100644 --- a/src/calibre/ebooks/metadata/book/json_codec.py +++ b/src/calibre/ebooks/metadata/book/json_codec.py @@ -117,8 +117,8 @@ class JsonCodec(object): def __init__(self): self.field_metadata = FieldMetadata() - def encode_to_file(self, file, booklist): - file.write(json.dumps(self.encode_booklist_metadata(booklist), + def encode_to_file(self, file_, booklist): + file_.write(json.dumps(self.encode_booklist_metadata(booklist), indent=2, encoding='utf-8')) def encode_booklist_metadata(self, booklist): @@ -156,21 +156,29 @@ class JsonCodec(object): else: return object_to_unicode(value) - def decode_from_file(self, file, booklist, book_class, prefix): + def decode_from_file(self, file_, booklist, book_class, prefix): js = [] try: - js = json.load(file, encoding='utf-8') + js = json.load(file_, encoding='utf-8') + self.raw_to_booklist(js, booklist, book_class, prefix) for item in js: - book = book_class(prefix, item.get('lpath', None)) - for key in item.keys(): - meta = self.decode_metadata(key, item[key]) - if key == 'user_metadata': - book.set_all_user_metadata(meta) - else: - if key == 'classifiers': - key = 'identifiers' - setattr(book, key, meta) - booklist.append(book) + booklist.append(self.raw_to_book(item, book_class, prefix)) + except: + print 'exception during JSON decode_from_file' + traceback.print_exc() + + def raw_to_book(self, json_book, book_class, prefix): + try: + book = book_class(prefix, json_book.get('lpath', None)) + for key,val in json_book.iteritems(): + meta = self.decode_metadata(key, val) + if key == 'user_metadata': + book.set_all_user_metadata(meta) + else: + if key == 'classifiers': + key = 'identifiers' + setattr(book, key, meta) + return book except: print 'exception during JSON decoding' traceback.print_exc() diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 1dcadf7b65..da2c45fd9c 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -144,6 +144,7 @@ class DeviceManager(Thread): # {{{ self.open_feedback_msg = open_feedback_msg self._device_information = None self.current_library_uuid = None + self.call_shutdown_on_disconnect = False def report_progress(self, *args): pass @@ -197,6 +198,13 @@ class DeviceManager(Thread): # {{{ self.ejected_devices.remove(self.connected_device) else: self.connected_slot(False, self.connected_device_kind) + if self.call_shutdown_on_disconnect: + # The current device is an instance of a plugin class instantiated + # to handle this connection, probably as a mounted device. We are + # now abandoning the instance that we created, so we tell it that it + # is being shut down. + self.connected_device.shutdown() + self.call_shutdown_on_disconnect = False self.connected_device = None self._device_information = None @@ -265,7 +273,20 @@ class DeviceManager(Thread): # {{{ except Queue.Empty: pass + def run_startup(self, dev): + name = 'unknown' + try: + name = dev.__class__.__name__ + dev.startup() + except: + prints('Startup method for device %s threw exception'%name) + traceback.print_exc() + def run(self): + # Do any device-specific startup processing. + for d in self.devices: + self.run_startup(d) + while self.keep_going: kls = None while True: @@ -277,6 +298,11 @@ class DeviceManager(Thread): # {{{ if kls is not None: try: dev = kls(folder_path) + # We just created a new device instance. Call its startup + # method and set the flag to call the shutdown method when + # it disconnects. + self.run_startup(dev) + self.call_shutdown_on_disconnect = True self.do_connect([[dev, None],], device_kind=device_kind) except: prints('Unable to open %s as device (%s)'%(device_kind, folder_path)) @@ -295,6 +321,13 @@ class DeviceManager(Thread): # {{{ break time.sleep(self.sleep_time) + # We are exiting. Call the shutdown method for each plugin + for p in self.devices: + try: + p.shutdown() + except: + pass + def create_job_step(self, func, done, description, to_job, args=[], kwargs={}): job = DeviceJob(func, done, self.job_manager, args=args, kwargs=kwargs, description=description) @@ -934,11 +967,6 @@ class DeviceMixin(object): # {{{ fmt = None if specific: - if (not self.device_connected or not self.device_manager or - self.device_manager.device is None): - error_dialog(self, _('No device'), - _('No device connected'), show=True) - return formats = [] aval_out_formats = available_output_formats() format_count = {} diff --git a/src/calibre/gui2/device_drivers/configwidget.py b/src/calibre/gui2/device_drivers/configwidget.py index 94843f90e3..b47a80b6ad 100644 --- a/src/calibre/gui2/device_drivers/configwidget.py +++ b/src/calibre/gui2/device_drivers/configwidget.py @@ -43,6 +43,9 @@ class ConfigWidget(QWidget, Ui_ConfigWidget): self.connect(self.column_up, SIGNAL('clicked()'), self.up_column) self.connect(self.column_down, SIGNAL('clicked()'), self.down_column) + if device.HIDE_FORMATS_CONFIG_BOX: + self.groupBox.hide() + if supports_subdirs: self.opt_use_subdirs.setChecked(self.settings.use_subdirs) else: diff --git a/src/calibre/gui2/device_drivers/configwidget.ui b/src/calibre/gui2/device_drivers/configwidget.ui index 92324fd1a7..d8c3c44e22 100644 --- a/src/calibre/gui2/device_drivers/configwidget.ui +++ b/src/calibre/gui2/device_drivers/configwidget.ui @@ -103,6 +103,19 @@ + + + + Qt::Vertical + + + + 20 + 40 + + + + diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py index 0b5fead634..a61d2dcc3e 100644 --- a/src/calibre/library/server/base.py +++ b/src/calibre/library/server/base.py @@ -17,7 +17,7 @@ from calibre.utils.date import fromtimestamp from calibre.library.server import listen_on, log_access_file, log_error_file from calibre.library.server.utils import expose, AuthController from calibre.utils.mdns import publish as publish_zeroconf, \ - stop_server as stop_zeroconf, get_external_ip + unpublish as unpublish_zeroconf, get_external_ip from calibre.library.server.content import ContentServer from calibre.library.server.mobile import MobileServer from calibre.library.server.xml import XMLServer @@ -94,7 +94,10 @@ class BonJour(SimplePlugin): # {{{ def stop(self): try: - stop_zeroconf() + unpublish_zeroconf('Books in calibre', '_stanza._tcp', + self.port, {'path':self.prefix+'/stanza'}) + unpublish_zeroconf('Books in calibre', '_calibre._tcp', + self.port, {'path':self.prefix+'/opds'}) except: import traceback cherrypy.log.error('Failed to stop BonJour:') diff --git a/src/calibre/utils/Zeroconf.py b/src/calibre/utils/Zeroconf.py index 0e55e8f516..b722865101 100755 --- a/src/calibre/utils/Zeroconf.py +++ b/src/calibre/utils/Zeroconf.py @@ -871,6 +871,8 @@ class Engine(threading.Thread): from calibre.constants import DEBUG try: rr, wr, er = select.select(rs, [], [], self.timeout) + if globals()['_GLOBAL_DONE']: + continue for socket in rr: try: self.readers[socket].handle_read() @@ -1419,6 +1421,9 @@ class Zeroconf(object): i += 1 nextTime += _UNREGISTER_TIME + def countRegisteredServices(self): + return len(self.services) + def checkService(self, info): """Checks the network for a unique service name, modifying the ServiceInfo passed in if it is not unique.""" diff --git a/src/calibre/utils/mdns.py b/src/calibre/utils/mdns.py index 2be6bef49b..42e846577e 100644 --- a/src/calibre/utils/mdns.py +++ b/src/calibre/utils/mdns.py @@ -76,6 +76,33 @@ def publish(desc, type, port, properties=None, add_hostname=True): server=hostname+'.local.') server.registerService(service) +def unpublish(desc, type, port, properties=None, add_hostname=True): + ''' + Unpublish a service. + + The parameters must be the same as used in the corresponding call to publish + ''' + port = int(port) + server = start_server() + try: + hostname = socket.gethostname().partition('.')[0] + except: + hostname = 'Unknown' + + if add_hostname: + desc += ' (on %s)'%hostname + local_ip = get_external_ip() + type = type+'.local.' + from calibre.utils.Zeroconf import ServiceInfo + service = ServiceInfo(type, desc+'.'+type, + address=socket.inet_aton(local_ip), + port=port, + properties=properties, + server=hostname+'.local.') + server.unregisterService(service) + if server.countRegisteredServices() == 0: + stop_server() + def stop_server(): global _server if _server is not None: From 091164b307b29ed9e54b28868bb2bfbd872c9bcd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 24 Jul 2012 11:56:41 +0530 Subject: [PATCH 11/48] Refactor fs code in the viewer --- resources/compiled_coffeescript.zip | Bin 49166 -> 51021 bytes src/calibre/gui2/viewer/documentview.py | 28 +++++------------------- src/calibre/gui2/viewer/javascript.py | 4 +++- 3 files changed, 8 insertions(+), 24 deletions(-) diff --git a/resources/compiled_coffeescript.zip b/resources/compiled_coffeescript.zip index 7ec0f242309ec5f872fe9e527e47e71eaa145256..bb2ea7cfd95dcf122d4a96d2769f0e958940ea34 100644 GIT binary patch delta 1258 zcmb7E&ubGw6y7AQEs@|4wyjz#BNjIH$D%z1r3mZAOSH81AWCJKB(urNW_H<`Z4*j~ z7mpUg+?C!fQhE{5M3CailNT={{sr1wFM>14CcE3D0VnLjy!qaj?|u8`Z9h+Z-A<&I zvxy;D66^cg(f$|hO=>UsUfU;2Ifs4FM$aFt9cin#Ge7oDNs@T3O03=-J8gKDrIWmi z5N3b0nIQlvsKadX9*?iK!u5Ca;oaf*t>8?>u#CVRpadahF_{E>86M74)5ah;V!!V` z2vM9yVTDpY{s+XZ#G%?3vp4AuTn#EFI+*Vsy zphtCF*`_wFI!GmO9r@&h{aUyb&k^?cOu9BeL5`?xHTSu&juaPB&&5&U#}sw*9tx%H z#4X>oh&Zn8T1y;vksIS1nl8hNPEm#G8)ku)<^Vg)Oq^SXE+8EdrUBAC3fM3Jm!t#@Pg{{0CyWV5PiD>7nJk>)N?jgaq06-t_gmn?#NZJm#z;-uB*_1XC zzJ@onmCMmPh|xy)+U{tRsaRZ2bjJ&uZBDYkjS2R&DYKuAEAh7}F-(}uw`56XFPh#= mkkC7MT4Tz8eQSu+>5u^R8ClK_^AROU!~92MlJxmN?EV2HMyQ+s delta 118 zcmX@x$K2Pzym>wE5eH_zIZrq8uHoHW?3Yw9neUj! Date: Tue, 24 Jul 2012 15:30:05 +0530 Subject: [PATCH 12/48] E-book viewer full scrren mode: Allow clicking in the left and right page margins to turn pages. Fixes #1024819 ([Enhancement] Clic to advance page in viewer) --- resources/compiled_coffeescript.zip | Bin 51021 -> 52157 bytes .../ebooks/oeb/display/full_screen.coffee | 65 ++++++++++++++++++ src/calibre/ebooks/oeb/display/paged.coffee | 12 ++++ src/calibre/gui2/viewer/documentview.py | 19 ++++- src/calibre/gui2/viewer/main.py | 4 +- 5 files changed, 96 insertions(+), 4 deletions(-) create mode 100644 src/calibre/ebooks/oeb/display/full_screen.coffee diff --git a/resources/compiled_coffeescript.zip b/resources/compiled_coffeescript.zip index bb2ea7cfd95dcf122d4a96d2769f0e958940ea34..ed0bce537bf784ae6e934ee71cb5ad17fdd46862 100644 GIT binary patch delta 999 zcmb7DO=uHA6ke0k{Lu6e(qd}RiKfkNNmDV$CTS@aL~AX0u!6YRWM`ALaW~Fx5@LI> zUIY(Pd7zg<4<7U)RdQ9@8j^xo5IlHMD4r``1dA7)*{0U5f;cb>Gw<7PzW3huHt#uK zK6Hkr&pA5+99!RR?u^|okD#t`j$``>2Mev9`j&5d6vX6Vxv1u5MMWoKNy=l)^o4eepWs)BesPa0Wr;<=(qRn0&`?~fwHaCY9L9mnKD=28fT zFT3Hb#|7Ic-TylK)+0XY9_G_v;Tl9LILpsuKts-F5%!q!V5= z`r+;?fJh?(KhhpZce)aiEKjfkO{#{8HB9&@m6n^0#cNSa*4qIDNTic~bP03X&P zpfpaUs;VaI)i}Y|^$Px9mil62=@3jedM^I{#y=XDCA3@Eh#`?+Wv^4Ldb?FMC6dP` z3a8+{r!R=+=i61xmQ7RF=zR*Eq@{$_#4!iKkU0|wv~yt2kwsZGN=2y#%cnyLgG%{Z zkhSih-P#_5wWmQxmcaL69u}{7RVFOV^H~ySCP2v);+bwrY%_s?1_rEz7=nT=CkVp6 zWtnej)(8JXNrqFZY1;CgJbXoIgeZF3xfwPX$j1~XX@}ocPw~ywy Qt7{fe*f`pmc+CPo0h1D3VE_OC delta 238 zcmdlxo%w7Z^JZ74oxFT}4WAvF)fkN$9T*rI9VW**Np3WJV?TL?Rg?e# diff --git a/src/calibre/ebooks/oeb/display/full_screen.coffee b/src/calibre/ebooks/oeb/display/full_screen.coffee new file mode 100644 index 0000000000..f4dece210a --- /dev/null +++ b/src/calibre/ebooks/oeb/display/full_screen.coffee @@ -0,0 +1,65 @@ +#!/usr/bin/env coffee +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +### + Copyright 2012, Kovid Goyal + Released under the GPLv3 License +### + + +log = window.calibre_utils.log + +class FullScreen + # This class is a namespace to expose functions via the + # window.full_screen object. The most important functions are: + + constructor: () -> + if not this instanceof arguments.callee + throw new Error('FullScreen constructor called as function') + this.in_full_screen = false + this.initial_left_margin = null + this.initial_right_margin = null + + save_margins: () -> + bs = document.body.style + this.initial_left_margin = bs.marginLeft + this.initial_right_margin = bs.marginRight + + on: (max_text_width, in_paged_mode) -> + if in_paged_mode + window.paged_display.max_col_width = max_text_width + else + s = document.body.style + s.maxWidth = max_text_width + 'px' + s.marginLeft = 'auto' + s.marginRight = 'auto' + window.addEventListener('click', this.handle_click, false) + + off: (in_paged_mode) -> + window.removeEventListener('click', this.handle_click, false) + if in_paged_mode + window.paged_display.max_col_width = -1 + else + s = document.body.style + s.maxWidth = 'none' + if this.initial_left_margin != null + s.marginLeft = this.initial_left_margin + if this.initial_right_margin != null + s.marginRight = this.initial_right_margin + + handle_click: (event) -> + if event.target != document.documentElement or event.button != 0 + return + res = null + if window.paged_display.in_paged_mode + res = window.paged_display.click_for_page_turn(event) + else + br = document.body.getBoundingClientRect() + if not (br.left <= event.clientX <= br.right) + res = event.clientX < br.left + if res != null + window.py_bridge.page_turn_requested(res) + +if window? + window.full_screen = new FullScreen() + diff --git a/src/calibre/ebooks/oeb/display/paged.coffee b/src/calibre/ebooks/oeb/display/paged.coffee index 3bb3461927..c2a09b5587 100644 --- a/src/calibre/ebooks/oeb/display/paged.coffee +++ b/src/calibre/ebooks/oeb/display/paged.coffee @@ -395,6 +395,18 @@ class PagedDisplay log('Viewport cfi:', ans) return ans + click_for_page_turn: (event) -> + # Check if the click event event should generate a apge turn. Returns + # null if it should not, true if it is a backwards page turn, false if + # it is a forward apge turn. + left_boundary = this.current_margin_side + right_bondary = this.screen_width - this.current_margin_side + if left_boundary > event.clientX + return true + if right_bondary < event.clientX + return false + return null + if window? window.paged_display = new PagedDisplay() diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 1afb284e8f..dac9a05113 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -11,7 +11,7 @@ from functools import partial from PyQt4.Qt import (QSize, QSizePolicy, QUrl, SIGNAL, Qt, pyqtProperty, QPainter, QPalette, QBrush, QFontDatabase, QDialog, QColor, QPoint, QImage, QRegion, QIcon, pyqtSignature, QAction, QMenu, QString, - pyqtSignal, QSwipeGesture, QApplication) + pyqtSignal, QSwipeGesture, QApplication, pyqtSlot) from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings from calibre.gui2.viewer.flip import SlideFlip @@ -34,6 +34,8 @@ def load_builtin_fonts(): class Document(QWebPage): # {{{ + page_turn = pyqtSignal(object) + def set_font_settings(self): opts = config().parse() settings = self.settings() @@ -171,6 +173,10 @@ class Document(QWebPage): # {{{ if not isxp and self.hyphenate and getattr(self, 'loaded_lang', ''): self.javascript('do_hyphenation("%s")'%self.loaded_lang) + @pyqtSlot(int) + def page_turn_requested(self, backwards): + self.page_turn.emit(bool(backwards)) + def _pass_json_value_getter(self): val = json.dumps(self.bridge_value) return QString(val) @@ -187,10 +193,10 @@ class Document(QWebPage): # {{{ self.fit_images() self.init_hyphenate() self.javascript('full_screen.save_margins()') - if self.in_paged_mode: - self.switch_to_paged_mode() if self.in_fullscreen_mode: self.switch_to_fullscreen_mode() + if self.in_paged_mode: + self.switch_to_paged_mode() self.read_anchor_positions(use_cache=False) self.first_load = False @@ -445,6 +451,7 @@ class DocumentView(QWebView): # {{{ self.connect(self.document, SIGNAL('selectionChanged()'), self.selection_changed) self.connect(self.document, SIGNAL('animated_scroll_done()'), self.animated_scroll_done, Qt.QueuedConnection) + self.document.page_turn.connect(self.page_turn_requested) copy_action = self.pageAction(self.document.Copy) copy_action.setIcon(QIcon(I('convert.png'))) d = self.document @@ -878,6 +885,12 @@ class DocumentView(QWebView): # {{{ self.manager.scrolled(self.scroll_fraction) #print 'After all:', self.document.ypos + def page_turn_requested(self, backwards): + if backwards: + self.previous_page() + else: + self.next_page() + def scroll_by(self, x=0, y=0, notify=True): old_pos = (self.document.xpos if self.document.in_paged_mode else self.document.ypos) diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index 65be08343d..c6eb76c735 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -272,9 +272,11 @@ class EbookViewer(MainWindow, Ui_EbookViewer):

%s

%s

%s

+

%s

'''%(_('Full screen mode'), _('Right click to show controls'), + _('Tap in the left or right page margin to turn pages'), _('Press Esc to quit')), self) self.full_screen_label.setVisible(False) @@ -496,7 +498,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): a.setStartValue(QSize(width, 0)) a.setEndValue(QSize(width, height)) a.start() - QTimer.singleShot(2750, self.full_screen_label.hide) + QTimer.singleShot(3500, self.full_screen_label.hide) self.view.document.switch_to_fullscreen_mode() if self.view.document.fullscreen_clock: self.show_clock() From 76c0892b510f639b6638ac4edce48eca0d458b0c Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 24 Jul 2012 14:18:52 +0200 Subject: [PATCH 13/48] Add an interface to permit starting and stopping of devices without disabling them. Will be used by the smartdevice driver --- src/calibre/devices/interface.py | 48 ++++++++++++++++++++++++ src/calibre/gui2/actions/device.py | 35 ++++++++++++++++- src/calibre/gui2/device.py | 60 ++++++++++++++++++++++++++++++ src/calibre/gui2/ui.py | 9 +++++ src/calibre/utils/Zeroconf.py | 15 +++++--- 5 files changed, 160 insertions(+), 7 deletions(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 26239b59e7..0f2027065e 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -514,6 +514,54 @@ class DevicePlugin(Plugin): ''' pass + # Dynamic control interface + + def is_dynamically_controllable(self): + ''' + Called by the device manager when starting plugins. If this method returns + a string, then a) it supports the device manager's dynamic control + interface, and b) that name is to be used when talking to the plugin + ''' + return None + + def start_plugin(self): + ''' + This method is called to start the plugin. The plugin should begin + to accept device connections however it does that. If the plugin is + already accepting connections, then do nothing. + ''' + pass + + def stop_plugin(self): + ''' + This method is called to stop the plugin. The plugin should no longer + accept connections, and should cleanup behind itself. It is likely that + this method should call shutdown. If the plugin is already not accepting + connections, then do nothing. + ''' + pass + + def get_option(self, opt_string): + ''' + Return the value of the option indicated by opt_string. This method can + be called when the plugin is not started. Return None if the option does + not exist. + ''' + return None + + def set_option(self, opt_string, opt_value): + ''' + Set the value of the option indicated by opt_string. This method can + be called when the plugin is not started. + ''' + pass + + def is_running(self): + ''' + Return True if the plugin is started, otherwise false + ''' + return False + class BookList(list): ''' A list of books. Each Book object must have the fields diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index 0ef06d59d5..afbf2584ed 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -24,6 +24,7 @@ class ShareConnMenu(QMenu): # {{{ config_email = pyqtSignal() toggle_server = pyqtSignal() + toggle_smartdevice = pyqtSignal() dont_add_to = frozenset(['context-menu-device']) def __init__(self, parent=None): @@ -56,6 +57,11 @@ class ShareConnMenu(QMenu): # {{{ _('Start Content Server')) self.toggle_server_action.triggered.connect(lambda x: self.toggle_server.emit()) + self.toggle_smartdevice_action = \ + self.addAction(QIcon(I('devices/galaxy_s3.png')), + _('Start Smart Device Connections')) + self.toggle_smartdevice_action.triggered.connect(lambda x: + self.toggle_smartdevice.emit()) self.addSeparator() self.email_actions = [] @@ -80,6 +86,15 @@ class ShareConnMenu(QMenu): # {{{ text = _('Stop Content Server') + ' [%s]'%get_external_ip() self.toggle_server_action.setText(text) + def smartdevice_state_changed(self, accepting): + if accepting: + self.toggle_smartdevice_action.setText(_('Stop Smart Device Connections')) + else: + self.toggle_smartdevice_action.setText(_('Start Smart Device Connections')) + + def hide_smartdevice_menus(self): + self.toggle_smartdevice_action.setVisible(False) + def build_email_entries(self, sync_menu): from calibre.gui2.device import DeviceAction for ac in self.email_actions: @@ -158,6 +173,7 @@ class ConnectShareAction(InterfaceAction): def genesis(self): self.share_conn_menu = ShareConnMenu(self.gui) self.share_conn_menu.toggle_server.connect(self.toggle_content_server) + self.share_conn_menu.toggle_smartdevice.connect(self.toggle_smartdevice) self.share_conn_menu.config_email.connect(partial( self.gui.iactions['Preferences'].do_config, initial_plugin=('Sharing', 'Email'))) @@ -200,8 +216,23 @@ class ConnectShareAction(InterfaceAction): if not self.stopping_msg.isVisible(): self.stopping_msg.exec_() return - - self.gui.content_server = None self.stopping_msg.accept() + def toggle_smartdevice(self): + info_dialog(self.gui, _('Foobar'), + _('Start server bla bla blah...'), + show_copy_button=False, show=True) + if self.gui.device_manager.is_running('smartdevice'): + self.gui.device_manager.stop_plugin('smartdevice') + else: + self.gui.device_manager.start_plugin('smartdevice') + self.share_conn_menu.smartdevice_state_changed( + self.gui.device_manager.is_running('smartdevice')) + + def smartdevice_state_changed(self, running): + self.share_conn_menu.smartdevice_state_changed(running) + + def check_smartdevice_menus(self): + if not self.gui.device_manager.is_enabled('smartdevice'): + self.share_conn_menu.hide_smartdevice_menus() diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 17f1e47853..14a9093bd4 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -145,6 +145,8 @@ class DeviceManager(Thread): # {{{ self._device_information = None self.current_library_uuid = None self.call_shutdown_on_disconnect = False + self.devices_initialized = Queue.Queue(0) + self.dynamic_plugins = {} def report_progress(self, *args): pass @@ -286,6 +288,10 @@ class DeviceManager(Thread): # {{{ # Do any device-specific startup processing. for d in self.devices: self.run_startup(d) + n = d.is_dynamically_controllable() + if n: + self.dynamic_plugins[n] = d + self.devices_initialized.put(None) while self.keep_going: kls = None @@ -508,6 +514,59 @@ class DeviceManager(Thread): # {{{ if self.connected_device: self.connected_device.set_driveinfo_name(location_code, name) + # dynamic plugin interface + + def start_plugin(self, name): + try: + d = self.dynamic_plugins.get(name, None) + if d: + d.start_plugin() + except: + pass + + def stop_plugin(self, name): + try: + d = self.dynamic_plugins.get(name, None) + if d: + d.stop_plugin() + except: + pass + + def get_option(self, name, opt_string): + try: + d = self.dynamic_plugins.get(name, None) + if d: + return d.get_option(opt_string) + except: + pass + return None + + def set_option(self, name, opt_string, opt_value): + try: + d = self.dynamic_plugins.get(name, None) + if d: + d.set_option(opt_string, opt_value) + except: + pass + + def is_running(self, name): + try: + d = self.dynamic_plugins.get(name, None) + if d: + return d.is_running() + except: + pass + return False + + def is_enabled(self, name): + try: + d = self.dynamic_plugins.get(name, None) + if d: + return True + except: + pass + return False + # }}} class DeviceAction(QAction): # {{{ @@ -708,6 +767,7 @@ class DeviceMixin(object): # {{{ self.job_manager, Dispatcher(self.status_bar.show_message), Dispatcher(self.show_open_feedback)) self.device_manager.start() + self.device_manager.devices_initialized.get() if tweaks['auto_connect_to_folder']: self.connect_to_folder_named(tweaks['auto_connect_to_folder']) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index a597445f43..be86e91c29 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -337,6 +337,15 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ if config['autolaunch_server']: self.start_content_server() + smartdevice_actions = self.iactions['Connect Share'] + smartdevice_actions.check_smartdevice_menus() + if self.device_manager.get_option('smartdevice', 'autostart'): + try: + self.device_manager.start_plugin('smartdevice') + smartdevice_actions.smartdevice_state_changed(True) + except: + pass + self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) self.read_settings() diff --git a/src/calibre/utils/Zeroconf.py b/src/calibre/utils/Zeroconf.py index b722865101..1287148476 100755 --- a/src/calibre/utils/Zeroconf.py +++ b/src/calibre/utils/Zeroconf.py @@ -955,11 +955,16 @@ class Reaper(threading.Thread): return if globals()['_GLOBAL_DONE']: return - now = currentTimeMillis() - for record in self.zeroconf.cache.entries(): - if record.isExpired(now): - self.zeroconf.updateRecord(now, record) - self.zeroconf.cache.remove(record) + try: + # can get here in a race condition with shutdown. Swallow the + # exception and run around the loop again. + now = currentTimeMillis() + for record in self.zeroconf.cache.entries(): + if record.isExpired(now): + self.zeroconf.updateRecord(now, record) + self.zeroconf.cache.remove(record) + except: + pass class ServiceBrowser(threading.Thread): From b2f1c6294b2b68bd7ae21cf54d9707f8b5a4a74c Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 24 Jul 2012 16:42:59 +0200 Subject: [PATCH 14/48] Implement the smartdevice control dialog. --- src/calibre/gui2/actions/device.py | 10 +- src/calibre/gui2/dialogs/smartdevice.py | 78 ++++++++++++++ src/calibre/gui2/dialogs/smartdevice.ui | 129 ++++++++++++++++++++++++ 3 files changed, 210 insertions(+), 7 deletions(-) create mode 100644 src/calibre/gui2/dialogs/smartdevice.py create mode 100644 src/calibre/gui2/dialogs/smartdevice.ui diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index afbf2584ed..8faf9f1717 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -14,6 +14,7 @@ from calibre.utils.smtp import config as email_config from calibre.constants import iswindows, isosx from calibre.customize.ui import is_disabled from calibre.devices.bambook.driver import BAMBOOK +from calibre.gui2.dialogs.smartdevice import SmartdeviceDialog from calibre.gui2 import info_dialog class ShareConnMenu(QMenu): # {{{ @@ -220,13 +221,8 @@ class ConnectShareAction(InterfaceAction): self.stopping_msg.accept() def toggle_smartdevice(self): - info_dialog(self.gui, _('Foobar'), - _('Start server bla bla blah...'), - show_copy_button=False, show=True) - if self.gui.device_manager.is_running('smartdevice'): - self.gui.device_manager.stop_plugin('smartdevice') - else: - self.gui.device_manager.start_plugin('smartdevice') + sd_dialog = SmartdeviceDialog(self.gui) + sd_dialog.exec_() self.share_conn_menu.smartdevice_state_changed( self.gui.device_manager.is_running('smartdevice')) diff --git a/src/calibre/gui2/dialogs/smartdevice.py b/src/calibre/gui2/dialogs/smartdevice.py new file mode 100644 index 0000000000..63c49a5fc7 --- /dev/null +++ b/src/calibre/gui2/dialogs/smartdevice.py @@ -0,0 +1,78 @@ +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal ' +import re +from PyQt4.QtGui import QDialog, QLineEdit +from PyQt4.QtCore import SIGNAL, Qt + +from calibre.gui2.dialogs.smartdevice_ui import Ui_Dialog +from calibre.gui2 import dynamic + +class SmartdeviceDialog(QDialog, Ui_Dialog): + + def __init__(self, parent): + QDialog.__init__(self, parent) + Ui_Dialog.__init__(self) + self.setupUi(self) + + self.msg.setText( + _('This dialog starts and stops the smart device app interface. ' + 'When you start the interface, you might see some messages from ' + 'your computer\'s firewall or anti-virus manager asking you ' + 'if it is OK for calibre to connect to the network. Please ' + 'answer yes. If you do not, the app will not work. It will ' + 'be unable to connect to calibre.')) + + self.passwd_msg.setText( + _('Use a password if calibre is running on a network that ' + 'is not secure. For example, if you run calibre on a laptop, ' + 'use that laptop in an airport, and want to connect your ' + 'smart device to calibre, you should use a password.')) + + self.auto_start_msg.setText( + _('Check this box if you want calibre to automatically start the ' + 'smart device interface when calibre starts. You should not do ' + 'this if you are using a network that is not secure and you ' + 'are not setting a password.')) + self.connect(self.show_password, SIGNAL('stateChanged(int)'), self.toggle_password) + + self.device_manager = parent.device_manager + if self.device_manager.get_option('smartdevice', 'autostart'): + self.autostart_box.setChecked(True) + pw = self.device_manager.get_option('smartdevice', 'password') + if pw: + self.password_box.setText(pw) + + if self.device_manager.is_running('smartdevice'): + self.start_button.setEnabled(False) + self.stop_button.setEnabled(True) + else: + self.start_button.setEnabled(True) + self.stop_button.setEnabled(False) + self.start_button.clicked.connect(self.start_button_clicked) + self.stop_button.clicked.connect(self.stop_button_clicked) + self.cancel_button.clicked.connect(self.cancel_button_clicked) + self.OK_button.clicked.connect(self.accept) + + def start_button_clicked(self): + self.device_manager.start_plugin('smartdevice') + self.accept() + + def stop_button_clicked(self): + self.device_manager.stop_plugin('smartdevice') + self.accept() + + def cancel_button_clicked(self): + QDialog.reject(self) + + def toggle_password(self, state): + if state == Qt.Unchecked: + self.password_box.setEchoMode(QLineEdit.Password) + else: + self.password_box.setEchoMode(QLineEdit.Normal) + + def accept(self): + self.device_manager.set_option('smartdevice', 'password', + unicode(self.password_box.text())) + self.device_manager.set_option('smartdevice', 'autostart', + self.autostart_box.isChecked()) + QDialog.accept(self) diff --git a/src/calibre/gui2/dialogs/smartdevice.ui b/src/calibre/gui2/dialogs/smartdevice.ui new file mode 100644 index 0000000000..26795249be --- /dev/null +++ b/src/calibre/gui2/dialogs/smartdevice.ui @@ -0,0 +1,129 @@ + + + Dialog + + + + 0 + 0 + 600 + 209 + + + + Smart device control + + + + :/images/mimetypes/unknown.png:/images/mimetypes/unknown.png + + + + + + TextLabel + + + true + + + + + + + &Password: + + + password_box + + + + + + + QLineEdit::Password + + + + + + + TextLabel + + + true + + + + 100 + 0 + + + + + + + + &Show password + + + + + + + &Auto-start + + + + + + + true + + + + + + + All the buttons except Cancel will save the above settings + + + + + + + + + Start interface + + + + + + + Stop interface + + + + + + + OK + + + + + + + Cancel + + + + + + + + + + + From 0f16e4d9c7006c2be08993f5e325f75432c7fea5 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 24 Jul 2012 18:14:22 +0200 Subject: [PATCH 15/48] Improved smartdevice control dialog --- src/calibre/gui2/actions/device.py | 27 ++---- src/calibre/gui2/dialogs/smartdevice.py | 51 +++++------ src/calibre/gui2/dialogs/smartdevice.ui | 110 +++++++++++++----------- src/calibre/gui2/ui.py | 1 - 4 files changed, 93 insertions(+), 96 deletions(-) diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index 8faf9f1717..8d08f53f0a 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -25,7 +25,7 @@ class ShareConnMenu(QMenu): # {{{ config_email = pyqtSignal() toggle_server = pyqtSignal() - toggle_smartdevice = pyqtSignal() + control_smartdevice = pyqtSignal() dont_add_to = frozenset(['context-menu-device']) def __init__(self, parent=None): @@ -58,11 +58,11 @@ class ShareConnMenu(QMenu): # {{{ _('Start Content Server')) self.toggle_server_action.triggered.connect(lambda x: self.toggle_server.emit()) - self.toggle_smartdevice_action = \ + self.control_smartdevice_action = \ self.addAction(QIcon(I('devices/galaxy_s3.png')), - _('Start Smart Device Connections')) - self.toggle_smartdevice_action.triggered.connect(lambda x: - self.toggle_smartdevice.emit()) + _('Control Smart Device Connections')) + self.control_smartdevice_action.triggered.connect(lambda x: + self.control_smartdevice.emit()) self.addSeparator() self.email_actions = [] @@ -87,14 +87,8 @@ class ShareConnMenu(QMenu): # {{{ text = _('Stop Content Server') + ' [%s]'%get_external_ip() self.toggle_server_action.setText(text) - def smartdevice_state_changed(self, accepting): - if accepting: - self.toggle_smartdevice_action.setText(_('Stop Smart Device Connections')) - else: - self.toggle_smartdevice_action.setText(_('Start Smart Device Connections')) - def hide_smartdevice_menus(self): - self.toggle_smartdevice_action.setVisible(False) + self.control_smartdevice_action.setVisible(False) def build_email_entries(self, sync_menu): from calibre.gui2.device import DeviceAction @@ -174,7 +168,7 @@ class ConnectShareAction(InterfaceAction): def genesis(self): self.share_conn_menu = ShareConnMenu(self.gui) self.share_conn_menu.toggle_server.connect(self.toggle_content_server) - self.share_conn_menu.toggle_smartdevice.connect(self.toggle_smartdevice) + self.share_conn_menu.control_smartdevice.connect(self.control_smartdevice) self.share_conn_menu.config_email.connect(partial( self.gui.iactions['Preferences'].do_config, initial_plugin=('Sharing', 'Email'))) @@ -220,14 +214,9 @@ class ConnectShareAction(InterfaceAction): self.gui.content_server = None self.stopping_msg.accept() - def toggle_smartdevice(self): + def control_smartdevice(self): sd_dialog = SmartdeviceDialog(self.gui) sd_dialog.exec_() - self.share_conn_menu.smartdevice_state_changed( - self.gui.device_manager.is_running('smartdevice')) - - def smartdevice_state_changed(self, running): - self.share_conn_menu.smartdevice_state_changed(running) def check_smartdevice_menus(self): if not self.gui.device_manager.is_enabled('smartdevice'): diff --git a/src/calibre/gui2/dialogs/smartdevice.py b/src/calibre/gui2/dialogs/smartdevice.py index 63c49a5fc7..15b40d1077 100644 --- a/src/calibre/gui2/dialogs/smartdevice.py +++ b/src/calibre/gui2/dialogs/smartdevice.py @@ -22,47 +22,45 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): 'answer yes. If you do not, the app will not work. It will ' 'be unable to connect to calibre.')) - self.passwd_msg.setText( + self.password_box.setToolTip('

' + _('Use a password if calibre is running on a network that ' 'is not secure. For example, if you run calibre on a laptop, ' 'use that laptop in an airport, and want to connect your ' - 'smart device to calibre, you should use a password.')) + 'smart device to calibre, you should use a password.') + '

') - self.auto_start_msg.setText( + self.run_box.setToolTip('

' + + _('Check this box to allow calibre to accept connections from the ' + 'smart device. Uncheck the box to prevent connections.') + '

') + + self.autostart_box.setToolTip('

' + _('Check this box if you want calibre to automatically start the ' 'smart device interface when calibre starts. You should not do ' 'this if you are using a network that is not secure and you ' - 'are not setting a password.')) + 'are not setting a password.') + '

') self.connect(self.show_password, SIGNAL('stateChanged(int)'), self.toggle_password) + self.autostart_box.stateChanged.connect(self.autostart_changed) self.device_manager = parent.device_manager + if self.device_manager.is_running('smartdevice'): + self.run_box.setChecked(True) + else: + self.run_box.setChecked(False) + if self.device_manager.get_option('smartdevice', 'autostart'): self.autostart_box.setChecked(True) + self.run_box.setChecked(True) + self.run_box.setEnabled(False) + pw = self.device_manager.get_option('smartdevice', 'password') if pw: self.password_box.setText(pw) - if self.device_manager.is_running('smartdevice'): - self.start_button.setEnabled(False) - self.stop_button.setEnabled(True) + def autostart_changed(self): + if self.autostart_box.isChecked(): + self.run_box.setChecked(True) + self.run_box.setEnabled(False) else: - self.start_button.setEnabled(True) - self.stop_button.setEnabled(False) - self.start_button.clicked.connect(self.start_button_clicked) - self.stop_button.clicked.connect(self.stop_button_clicked) - self.cancel_button.clicked.connect(self.cancel_button_clicked) - self.OK_button.clicked.connect(self.accept) - - def start_button_clicked(self): - self.device_manager.start_plugin('smartdevice') - self.accept() - - def stop_button_clicked(self): - self.device_manager.stop_plugin('smartdevice') - self.accept() - - def cancel_button_clicked(self): - QDialog.reject(self) + self.run_box.setEnabled(True) def toggle_password(self, state): if state == Qt.Unchecked: @@ -75,4 +73,9 @@ class SmartdeviceDialog(QDialog, Ui_Dialog): unicode(self.password_box.text())) self.device_manager.set_option('smartdevice', 'autostart', self.autostart_box.isChecked()) + if self.run_box.isChecked(): + self.device_manager.start_plugin('smartdevice') + else: + self.device_manager.stop_plugin('smartdevice') + QDialog.accept(self) diff --git a/src/calibre/gui2/dialogs/smartdevice.ui b/src/calibre/gui2/dialogs/smartdevice.ui index 26795249be..97b4b71c00 100644 --- a/src/calibre/gui2/dialogs/smartdevice.ui +++ b/src/calibre/gui2/dialogs/smartdevice.ui @@ -18,7 +18,7 @@ :/images/mimetypes/unknown.png:/images/mimetypes/unknown.png
- + TextLabel @@ -43,16 +43,6 @@ QLineEdit::Password - - - - - - TextLabel - - - true - 100 @@ -69,61 +59,77 @@ + + + &Allow connections + + + + - &Auto-start + &Automatically allow connections at startup - - - - true + + + + + 0 + 100 + - - - - All the buttons except Cancel will save the above settings + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok - - - - - - Start interface - - - - - - - Stop interface - - - - - - - OK - - - - - - - Cancel - - - - -
+ + + buttonBox + accepted() + Dialog + accept() + + + 248 + 254 + + + 157 + 274 + + + + + buttonBox + rejected() + Dialog + reject() + + + 316 + 260 + + + 286 + 274 + + + +
diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index be86e91c29..8a7dfa1153 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -342,7 +342,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ if self.device_manager.get_option('smartdevice', 'autostart'): try: self.device_manager.start_plugin('smartdevice') - smartdevice_actions.smartdevice_state_changed(True) except: pass From 26d010d31580e7397b45005fa171dfa249332dd9 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 24 Jul 2012 19:23:45 +0200 Subject: [PATCH 16/48] Improved thread handling in device_manager dynamic plugin methods. Improved smartdevice dialog box. --- src/calibre/devices/interface.py | 19 ++++++-- src/calibre/gui2/device.py | 78 ++++++++++++++++---------------- 2 files changed, 55 insertions(+), 42 deletions(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 0f2027065e..1466732169 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -515,12 +515,15 @@ class DevicePlugin(Plugin): pass # Dynamic control interface + # All of these methods are called on the device_manager thread def is_dynamically_controllable(self): ''' Called by the device manager when starting plugins. If this method returns a string, then a) it supports the device manager's dynamic control - interface, and b) that name is to be used when talking to the plugin + interface, and b) that name is to be used when talking to the plugin. + + This method must be called from the device_manager thread. ''' return None @@ -529,6 +532,8 @@ class DevicePlugin(Plugin): This method is called to start the plugin. The plugin should begin to accept device connections however it does that. If the plugin is already accepting connections, then do nothing. + + This method must be called from the device_manager thread. ''' pass @@ -538,27 +543,35 @@ class DevicePlugin(Plugin): accept connections, and should cleanup behind itself. It is likely that this method should call shutdown. If the plugin is already not accepting connections, then do nothing. + + This method must be called from the device_manager thread. ''' pass - def get_option(self, opt_string): + def get_option(self, opt_string, default=None): ''' Return the value of the option indicated by opt_string. This method can be called when the plugin is not started. Return None if the option does not exist. + + This method must be called from the device_manager thread. ''' - return None + return default def set_option(self, opt_string, opt_value): ''' Set the value of the option indicated by opt_string. This method can be called when the plugin is not started. + + This method must be called from the device_manager thread. ''' pass def is_running(self): ''' Return True if the plugin is started, otherwise false + + This method must be called from the device_manager thread. ''' return False diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 14a9093bd4..63d7a03220 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal ' # Imports {{{ import os, traceback, Queue, time, cStringIO, re, sys -from threading import Thread +from threading import Thread, Event from PyQt4.Qt import (QMenu, QAction, QActionGroup, QIcon, SIGNAL, Qt, pyqtSignal, QDialog, QObject, QVBoxLayout, @@ -30,6 +30,7 @@ from calibre.constants import DEBUG from calibre.utils.config import prefs, tweaks from calibre.utils.magick.draw import thumbnail from calibre.library.save_to_disk import find_plugboard +from calibre.gui2 import is_gui_thread # }}} class DeviceJob(BaseJob): # {{{ @@ -145,8 +146,10 @@ class DeviceManager(Thread): # {{{ self._device_information = None self.current_library_uuid = None self.call_shutdown_on_disconnect = False - self.devices_initialized = Queue.Queue(0) + self.devices_initialized = Event() self.dynamic_plugins = {} + self.dynamic_plugin_requests = Queue.Queue(0) + self.dynamic_plugin_responses = Queue.Queue(0) def report_progress(self, *args): pass @@ -291,7 +294,7 @@ class DeviceManager(Thread): # {{{ n = d.is_dynamically_controllable() if n: self.dynamic_plugins[n] = d - self.devices_initialized.put(None) + self.devices_initialized.set() while self.keep_going: kls = None @@ -315,6 +318,7 @@ class DeviceManager(Thread): # {{{ traceback.print_exc() else: self.detect_device() + while True: job = self.next() if job is not None: @@ -325,8 +329,15 @@ class DeviceManager(Thread): # {{{ self.current_job = None else: break - time.sleep(self.sleep_time) - + while True: + dynamic_method = None + try: + (dynamic_method, args, kwargs) = \ + self.dynamic_plugin_requests.get(self.sleep_time) + res = dynamic_method(*args, **kwargs) + self.dynamic_plugin_responses.put(res) + except Queue.Empty: + break # We are exiting. Call the shutdown method for each plugin for p in self.devices: try: @@ -516,47 +527,36 @@ class DeviceManager(Thread): # {{{ # dynamic plugin interface - def start_plugin(self, name): + # This is a helper function that handles queueing with the device manager + def _queue_request(self, name, method, *args, **kwargs): + if not is_gui_thread(): + raise ValueError( + 'The device_manager dynamic plugin methods must be called from the GUI thread') try: d = self.dynamic_plugins.get(name, None) - if d: - d.start_plugin() + self.dynamic_plugin_requests.put((getattr(d, method), args, kwargs)) + return self.dynamic_plugin_responses.get() except: - pass - - def stop_plugin(self, name): - try: - d = self.dynamic_plugins.get(name, None) - if d: - d.stop_plugin() - except: - pass - - def get_option(self, name, opt_string): - try: - d = self.dynamic_plugins.get(name, None) - if d: - return d.get_option(opt_string) - except: - pass + traceback.print_exc() return None + # The dynamic plugin methods below must be called on the GUI thread. They + # will switch to the device thread before calling the plugin. + + def start_plugin(self, name): + self._queue_request(name, 'start_plugin') + + def stop_plugin(self, name): + self._queue_request(name, 'stop_plugin') + + def get_option(self, name, opt_string, default=None): + return self._queue_request(name, 'get_option', opt_string, default=default) + def set_option(self, name, opt_string, opt_value): - try: - d = self.dynamic_plugins.get(name, None) - if d: - d.set_option(opt_string, opt_value) - except: - pass + self._queue_request(name, 'set_option', opt_string, opt_value) def is_running(self, name): - try: - d = self.dynamic_plugins.get(name, None) - if d: - return d.is_running() - except: - pass - return False + return self._queue_request(name, 'is_running') def is_enabled(self, name): try: @@ -767,7 +767,7 @@ class DeviceMixin(object): # {{{ self.job_manager, Dispatcher(self.status_bar.show_message), Dispatcher(self.show_open_feedback)) self.device_manager.start() - self.device_manager.devices_initialized.get() + self.device_manager.devices_initialized.wait() if tweaks['auto_connect_to_folder']: self.connect_to_folder_named(tweaks['auto_connect_to_folder']) From 9d28e7a366e4538b57420dbb7224a518691f0032 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 24 Jul 2012 20:50:58 +0200 Subject: [PATCH 17/48] Make the menu icon for the smartdevice control change according to whether or not it is running. --- resources/images/dot_green.png | Bin 0 -> 1526 bytes resources/images/dot_red.png | Bin 0 -> 1450 bytes src/calibre/gui2/actions/device.py | 10 +++++++++- src/calibre/gui2/ui.py | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 resources/images/dot_green.png create mode 100644 resources/images/dot_red.png diff --git a/resources/images/dot_green.png b/resources/images/dot_green.png new file mode 100644 index 0000000000000000000000000000000000000000..c05376d7a78c5b5b0eb7e8155fac75d829445444 GIT binary patch literal 1526 zcmV*tc)l1%y>hAOt^11;26xB{c;^8L2dN#!<%<8xza2$uHEBN~N$SM@!O4 zB}dH}G_7Pb6eKGV91|7P`~qd=dqv#u_jcdwyhn>L?lV7jrvB(MXV2r@d(ZjZbMCwM z5uzxVPFf!>2>9uEW9UY62d`DVRVM9j#6XHPEClsHo0Bqg<;mhSODh z5TtoM`dZq~m#kM!PyqPwCq~xOVpCyn$#&_V4dMF1Y4QE$flFur&Q%YBU`Mmkz9E1l zSzxHHK#@UEcyTLK8P7_YScE<~^eK;8{SxOHVD-UPfa!6mu;=9V&C#Kw(_(w3fWv73 zUTAqp;0Hleq(7+XP$;>62+9pb+30ft`fOe3LP?Rr zxMQQREDYTZj+O6%TgFT4(Il5+&8rFkD{|G6it$nVmQ0SF40TLN$A~^-XGeDHk2P<6jI=A4gQ2>>mD&sN+vTGvv=Fu$j1-I843mDTBL9p~20|!2VOaz-+(Q8O%N^MFYu@eb*X^Mm0p@MdIA$eG zWLSp5)CAh-1mo7y8l>Q@@a?4(y1ki&Z)K>E$e|AE{7rUJlg8+{DZ z6UK+Ij0!iQ%c}q+L16}X*i&%i_P1|aJ(pm{7e4tZ!{f#V>-Biq@J|#-snMhgYK%rW zRDAO2?su(89s#C(=Cl0y$jHo?zJ2frwtE$Tp;;)sbQ%7*awUE5VyoxZaO$Vp5S@lK z%uY_iBbdW1IK8xb8joZKat`N%*=7&TU1n+fVCeubX`NP5o8yNJOpA^P0~==rZyd!M z#s|tPZ$aUibJEqk;J^yY{oeKfFMlip^w+ZN`P7s|>~(>If4WkrRV=Q7f*pAUxO&?F zG+p@deFcD&44wNivqx~(<&(z@!%rguEo;olN=Vilv~Q$eqev3(d`4b1jr#1LHzB{|Ja85!C!qSpOhFLtu`FwzUwwS~W1b{3vS->wrZ4< zxfdrALZCqK-K2S>sH!DIs+s2einZ48Ul?nb1`O&%;ao14>5(%a0Tit_x9x%2+FF&L zpC655D9i{!(=_RHI%#xDGYmtZCfsu=ijo|m$;IOsL8H+KI7h0H%WO9D?maMiiB3G8 zb^jyx0RVTCKqM`}xNQ{`6+~lWBM})HDchQxo7?&V0s_R~;9wEQh*42dZBB8%{451_ c+u8&C1?524-}?^}>;M1&07*qoM6N<$f}CvUw*UYD literal 0 HcmV?d00001 diff --git a/resources/images/dot_red.png b/resources/images/dot_red.png new file mode 100644 index 0000000000000000000000000000000000000000..88df5ccf1538f96e29aba049ed95279a9a3dcb67 GIT binary patch literal 1450 zcmV;b1y%ZqP)~6Q+UbaDKZ8eme6seSJDuRSSB>D$z6Uv3*%`|ZFjyF% z5v7%~iLY_>>wv3W1r%=sro1|{sZ5T6mW)B_?f|5IzX4iII*IBND82cdH539E*ggxe z>QXFMca*Z)4~fltfUjEvT0#O{3V|jC!BCZ1Co}_;=mt`B0VRl#9O?(D>nv#T=rQ#5 z&V&3f0Jt@0uo?|;ZNcUH?6SI6SIYr(CV>{3%*;@zZwLY?s|b{v2G$?eBXeF!pxu=~*{|WSY zFxP)xfE(Ma+2v8z<2~fs{5zUxSMxo>Zu0C7ZrRSqO>a0H^HtSK$lZZsJgcPashzrvjCrn%j;gYJ^2*geCpBD z{-AQWJP^C{D@Y?lr%?0ChC%?>wpdqC7IwIF)q0>DrTB9SFD>}oplnXiA^`{t^nsR^ zYc^hq|Ctw{XN&bP?{v01pI8fqrsD035+9@BA4Nm>x0{d*hufO2#NW#c(7oAu!Qpnd z*h~O9Rix;1fmjY=2oMPdFK)UTe>N|`)n;L=w8G=%mahauk&6q!u#g(L4S|U%-{!7F zbzXqW&4OHB>9JUBT(p$R{Y+iuQO@i$8dMt)!6n^a%odN>; zfB>IwD!rq-0Kmmf!dP{AnU}!sGqmDnfS^e9O2F9Uw6CSRKnZ?$MmSed>UfT2XwVh4 zr~oATl+ouzAaZ`^&xu`m0nRn?hn=Fey~Hknno1WHfaWYP6HP!^ins0RN#_3UG0%~$;rJ#8WkQN61@XeUFUxTV! zdkX=4^E97bw%Y~E;m0Z+phT0Iax7+;X=m&_{N;~8G?6yz-e&6e{Zatn>kVA)`48O= z;SFz@9h6uSn^J!Cau8)@B<`)2bg#ZV7W#Y(@BgFXl zIJK+q(+^6l%m7}( z@j@Yh4g~1m*G2||L1toN!u&XJ>>IuCu2WCGT8-r>75Pj#%)ZdDKeCUN`N&XMXS3dB z+5=J&#xU(6N;viHvYqX^u1g%pMMp zSs~qPs0|PA_Nb8-mqUOOCkG5o0mssrb17K`Mb%&?oPtm!0h6@j$Ggs&^CRN2I~Y8O zJ|=JuplJ-VpcDl70s#j0FU$Z*k|NGL&U8i0@2Lq7Z*$60Gi7Kcw60c~=TwsRYXlVv zv5xK$SM8;7!83(HRg|=*Y0*c{fC=zY-~Ar&`~8e4iZt3#m=S`eX;M{H8lBP>i-o|M znD2OsG995xQ_x1|d0xjeW{tF1ET&|8;PdsY@nUa*9xws2!vryB6DF%092_L3r>BYf z`udD66bjAnxm+%zs;bIB8>6A2Vcsd8&#cYBSzRu`-&1Jc-`{=mrT_o{07*qoM6N<$ Ef-$PXS^xk5 literal 0 HcmV?d00001 diff --git a/src/calibre/gui2/actions/device.py b/src/calibre/gui2/actions/device.py index 8d08f53f0a..92ed77e324 100644 --- a/src/calibre/gui2/actions/device.py +++ b/src/calibre/gui2/actions/device.py @@ -59,7 +59,7 @@ class ShareConnMenu(QMenu): # {{{ self.toggle_server_action.triggered.connect(lambda x: self.toggle_server.emit()) self.control_smartdevice_action = \ - self.addAction(QIcon(I('devices/galaxy_s3.png')), + self.addAction(QIcon(I('dot_green.png')), _('Control Smart Device Connections')) self.control_smartdevice_action.triggered.connect(lambda x: self.control_smartdevice.emit()) @@ -217,7 +217,15 @@ class ConnectShareAction(InterfaceAction): def control_smartdevice(self): sd_dialog = SmartdeviceDialog(self.gui) sd_dialog.exec_() + self.set_smartdevice_icon() def check_smartdevice_menus(self): if not self.gui.device_manager.is_enabled('smartdevice'): self.share_conn_menu.hide_smartdevice_menus() + + def set_smartdevice_icon(self): + running = self.gui.device_manager.is_running('smartdevice') + if running: + self.share_conn_menu.control_smartdevice_action.setIcon(QIcon(I('dot_green.png'))) + else: + self.share_conn_menu.control_smartdevice_action.setIcon(QIcon(I('dot_red.png'))) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 8a7dfa1153..b414ef04dd 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -344,6 +344,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ self.device_manager.start_plugin('smartdevice') except: pass + smartdevice_actions.set_smartdevice_icon() self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) From 40a273b316db7a719699af594b158975c6cc6834 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Tue, 24 Jul 2012 21:12:04 +0200 Subject: [PATCH 18/48] Fix sleep time parameter of dynamic plugin queue.get --- src/calibre/gui2/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 63d7a03220..75ec2f9a12 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -333,7 +333,7 @@ class DeviceManager(Thread): # {{{ dynamic_method = None try: (dynamic_method, args, kwargs) = \ - self.dynamic_plugin_requests.get(self.sleep_time) + self.dynamic_plugin_requests.get(timeout=self.sleep_time) res = dynamic_method(*args, **kwargs) self.dynamic_plugin_responses.put(res) except Queue.Empty: From 9180cf7d9a1f78991f584d664dd2b37b2c134879 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 25 Jul 2012 09:15:26 +0530 Subject: [PATCH 19/48] Fix #1028690 (Device not detected - Zeki TB782B) --- src/calibre/devices/android/driver.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index eca5d615ce..beee1082fb 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -194,7 +194,7 @@ class ANDROID(USBMS): 'GENERIC-', 'ZTE', 'MID', 'QUALCOMM', 'PANDIGIT', 'HYSTON', 'VIZIO', 'GOOGLE', 'FREESCAL', 'KOBO_INC', 'LENOVO', 'ROCKCHIP', 'POCKET', 'ONDA_MID', 'ZENITHIN', 'INGENIC', 'PMID701C', 'PD', - 'PMP5097C', 'MASS', 'NOVO7'] + 'PMP5097C', 'MASS', 'NOVO7', 'ZEKI'] WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897', 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', @@ -212,7 +212,7 @@ class ANDROID(USBMS): 'KTABLET_PC', 'INGENIC', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX', 'THINKPAD_TABLET', 'SGH-T989', 'YP-G70', 'STORAGE_DEVICE', - 'ADVANCED', 'SGH-I727'] + 'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER'] WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD', @@ -221,7 +221,8 @@ class ANDROID(USBMS): 'A1-07___C0541A4F', 'XT912', 'MB855', 'XT910', 'BOOK_A10_CARD', 'USB_2.0_DRIVER', 'I9100T', 'P999DW_SD_CARD', 'KTABLET_PC', 'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0_DRIVER', 'XT875', - 'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727'] + 'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727', + 'USB_FLASH_DRIVER'] OSX_MAIN_MEM = 'Android Device Main Memory' From 2aa4f7c70bb5749e714dc2d2b3044b1e872b292e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 25 Jul 2012 10:14:40 +0530 Subject: [PATCH 20/48] Conversion: Ignore invalid chapter detection and level n ToC expressions instead of erroring out --- .../ebooks/oeb/transforms/structure.py | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/src/calibre/ebooks/oeb/transforms/structure.py b/src/calibre/ebooks/oeb/transforms/structure.py index dd3db1415a..b90774bcc7 100644 --- a/src/calibre/ebooks/oeb/transforms/structure.py +++ b/src/calibre/ebooks/oeb/transforms/structure.py @@ -82,10 +82,17 @@ class DetectStructure(object): def detect_chapters(self): self.detected_chapters = [] + + def find_matches(expr, doc): + try: + return XPath(expr)(doc) + except: + self.log.warn('Invalid chapter expression, ignoring: %s'%expr) + return [] + if self.opts.chapter: - chapter_xpath = XPath(self.opts.chapter) for item in self.oeb.spine: - for x in chapter_xpath(item.data): + for x in find_matches(self.opts.chapter, item.data): self.detected_chapters.append((item, x)) chapter_mark = self.opts.chapter_mark @@ -164,11 +171,19 @@ class DetectStructure(object): added = OrderedDict() added2 = OrderedDict() counter = 1 + + def find_matches(expr, doc): + try: + return XPath(expr)(doc) + except: + self.log.warn('Invalid ToC expression, ignoring: %s'%expr) + return [] + for document in self.oeb.spine: previous_level1 = list(added.itervalues())[-1] if added else None previous_level2 = list(added2.itervalues())[-1] if added2 else None - for elem in XPath(self.opts.level1_toc)(document.data): + for elem in find_matches(self.opts.level1_toc, document.data): text, _href = self.elem_to_link(document, elem, counter) counter += 1 if text: @@ -178,7 +193,7 @@ class DetectStructure(object): #node.add(_('Top'), _href) if self.opts.level2_toc is not None and added: - for elem in XPath(self.opts.level2_toc)(document.data): + for elem in find_matches(self.opts.level2_toc, document.data): level1 = None for item in document.data.iterdescendants(): if item in added: @@ -196,7 +211,8 @@ class DetectStructure(object): break if self.opts.level3_toc is not None and added2: - for elem in XPath(self.opts.level3_toc)(document.data): + for elem in find_matches(self.opts.level3_toc, + document.data): level2 = None for item in document.data.iterdescendants(): if item in added2: From 1941541cdf4296fed1c9116f3328036b3939951c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 25 Jul 2012 10:18:37 +0530 Subject: [PATCH 21/48] ... --- src/calibre/ebooks/conversion/plumber.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/ebooks/conversion/plumber.py b/src/calibre/ebooks/conversion/plumber.py index 78821fa595..6a7b5e586b 100644 --- a/src/calibre/ebooks/conversion/plumber.py +++ b/src/calibre/ebooks/conversion/plumber.py @@ -326,7 +326,7 @@ OptionRecommendation(name='page_breaks_before', recommended_value="//*[name()='h1' or name()='h2']", level=OptionRecommendation.LOW, help=_('An XPath expression. Page breaks are inserted ' - 'before the specified elements.') + 'before the specified elements. To disable use the expression: /') ), OptionRecommendation(name='remove_fake_margins', From bd6acc80c6e9769155e84f9cc809006a5809c688 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 25 Jul 2012 07:28:11 +0200 Subject: [PATCH 22/48] Eliminate spurious exception message in dynamic control interface --- src/calibre/gui2/device.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 75ec2f9a12..8364e06f0f 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -534,8 +534,9 @@ class DeviceManager(Thread): # {{{ 'The device_manager dynamic plugin methods must be called from the GUI thread') try: d = self.dynamic_plugins.get(name, None) - self.dynamic_plugin_requests.put((getattr(d, method), args, kwargs)) - return self.dynamic_plugin_responses.get() + if d: + self.dynamic_plugin_requests.put((getattr(d, method), args, kwargs)) + return self.dynamic_plugin_responses.get() except: traceback.print_exc() return None @@ -556,7 +557,9 @@ class DeviceManager(Thread): # {{{ self._queue_request(name, 'set_option', opt_string, opt_value) def is_running(self, name): - return self._queue_request(name, 'is_running') + if self._queue_request(name, 'is_running'): + return True + return False def is_enabled(self, name): try: From f1a0e3ccb1709f792ee219ba6d46d9ec3dd289db Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 25 Jul 2012 08:23:19 +0200 Subject: [PATCH 23/48] Fix searching with localized strings that contain capital letters. --- src/calibre/library/caches.py | 50 ++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index e9bb6286f3..a516681fab 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -352,6 +352,14 @@ class ResultCache(SearchQueryParser): # {{{ '<=':[2, relop_le] } + local_today = ('_today', icu_lower(_('today'))) + local_yesterday = ('_yesterday', icu_lower(_('yesterday'))) + local_thismonth = ('_thismonth', icu_lower(_('thismonth'))) + local_daysago = icu_lower(_('daysago')) + local_daysago_len = len(local_daysago) + untrans_daysago = '_daysago' + untrans_daysago_len = len('_daysago') + def get_dates_matches(self, location, query, candidates): matches = set([]) if len(query) < 2: @@ -390,17 +398,24 @@ class ResultCache(SearchQueryParser): # {{{ if relop is None: (p, relop) = self.date_search_relops['='] - if query == _('today'): + if query in self.local_today: qd = now() field_count = 3 - elif query == _('yesterday'): + elif query in self.local_yesterday: qd = now() - timedelta(1) field_count = 3 - elif query == _('thismonth'): + elif query in self.local_thismonth: qd = now() field_count = 2 - elif query.endswith(_('daysago')): - num = query[0:-len(_('daysago'))] + elif query.endswith(self.local_daysago): + num = query[0:-self.local_daysago_len] + try: + qd = now() - timedelta(int(num)) + except: + raise ParseException(query, len(query), 'Number conversion error', self) + field_count = 3 + elif query.endswith(self.untrans_daysago): + num = query[0:-self.untrans_daysago_len] try: qd = now() - timedelta(int(num)) except: @@ -591,14 +606,23 @@ class ResultCache(SearchQueryParser): # {{{ query = icu_lower(query) return matchkind, query + local_no = icu_lower(_('no')) + local_yes = icu_lower(_('yes')) + local_unchecked = icu_lower(_('unchecked')) + local_checked = icu_lower(_('checked')) + local_empty = icu_lower(_('empty')) + local_blank = icu_lower(_('blank')) + local_bool_values = ( + local_no, local_unchecked, '_no', 'false', + local_yes, local_checked, '_yes', 'true', + local_empty, local_blank, '_empty') + def get_bool_matches(self, location, query, candidates): bools_are_tristate = self.db_prefs.get('bools_are_tristate') loc = self.field_metadata[location]['rec_index'] matches = set() query = icu_lower(query) - if query not in (_('no'), _('unchecked'), '_no', 'false', - _('yes'), _('checked'), '_yes', 'true', - _('empty'), _('blank'), '_empty'): + if query not in self.local_bool_values: raise ParseException(_('Invalid boolean query "{0}"').format(query)) for id_ in candidates: item = self._data[id_] @@ -608,20 +632,20 @@ class ResultCache(SearchQueryParser): # {{{ val = force_to_bool(item[loc]) if not bools_are_tristate: if val is None or not val: # item is None or set to false - if query in [_('no'), _('unchecked'), '_no', 'false']: + if query in (self.local_no, self.local_unchecked, '_no', 'false'): matches.add(item[0]) else: # item is explicitly set to true - if query in [_('yes'), _('checked'), '_yes', 'true']: + if query in (self.local_yes, self.local_checked, '_yes', 'true'): matches.add(item[0]) else: if val is None: - if query in [_('empty'), _('blank'), '_empty', 'false']: + if query in (self.local_empty, self.local_blank, '_empty', 'false'): matches.add(item[0]) elif not val: # is not None and false - if query in [_('no'), _('unchecked'), '_no', 'true']: + if query in (self.local_no, self.local_unchecked, '_no', 'true'): matches.add(item[0]) else: # item is not None and true - if query in [_('yes'), _('checked'), '_yes', 'true']: + if query in (self.local_yes, self.local_checked, '_yes', 'true'): matches.add(item[0]) return matches From daff566a56183296d9d58292e9db471b0721e6ac Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 25 Jul 2012 12:04:52 +0530 Subject: [PATCH 24/48] Fix boolean and date searching in non english calibre installs. --- src/calibre/library/caches.py | 50 ++++++++++++++++++++++++++--------- 1 file changed, 37 insertions(+), 13 deletions(-) diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py index e9bb6286f3..a516681fab 100644 --- a/src/calibre/library/caches.py +++ b/src/calibre/library/caches.py @@ -352,6 +352,14 @@ class ResultCache(SearchQueryParser): # {{{ '<=':[2, relop_le] } + local_today = ('_today', icu_lower(_('today'))) + local_yesterday = ('_yesterday', icu_lower(_('yesterday'))) + local_thismonth = ('_thismonth', icu_lower(_('thismonth'))) + local_daysago = icu_lower(_('daysago')) + local_daysago_len = len(local_daysago) + untrans_daysago = '_daysago' + untrans_daysago_len = len('_daysago') + def get_dates_matches(self, location, query, candidates): matches = set([]) if len(query) < 2: @@ -390,17 +398,24 @@ class ResultCache(SearchQueryParser): # {{{ if relop is None: (p, relop) = self.date_search_relops['='] - if query == _('today'): + if query in self.local_today: qd = now() field_count = 3 - elif query == _('yesterday'): + elif query in self.local_yesterday: qd = now() - timedelta(1) field_count = 3 - elif query == _('thismonth'): + elif query in self.local_thismonth: qd = now() field_count = 2 - elif query.endswith(_('daysago')): - num = query[0:-len(_('daysago'))] + elif query.endswith(self.local_daysago): + num = query[0:-self.local_daysago_len] + try: + qd = now() - timedelta(int(num)) + except: + raise ParseException(query, len(query), 'Number conversion error', self) + field_count = 3 + elif query.endswith(self.untrans_daysago): + num = query[0:-self.untrans_daysago_len] try: qd = now() - timedelta(int(num)) except: @@ -591,14 +606,23 @@ class ResultCache(SearchQueryParser): # {{{ query = icu_lower(query) return matchkind, query + local_no = icu_lower(_('no')) + local_yes = icu_lower(_('yes')) + local_unchecked = icu_lower(_('unchecked')) + local_checked = icu_lower(_('checked')) + local_empty = icu_lower(_('empty')) + local_blank = icu_lower(_('blank')) + local_bool_values = ( + local_no, local_unchecked, '_no', 'false', + local_yes, local_checked, '_yes', 'true', + local_empty, local_blank, '_empty') + def get_bool_matches(self, location, query, candidates): bools_are_tristate = self.db_prefs.get('bools_are_tristate') loc = self.field_metadata[location]['rec_index'] matches = set() query = icu_lower(query) - if query not in (_('no'), _('unchecked'), '_no', 'false', - _('yes'), _('checked'), '_yes', 'true', - _('empty'), _('blank'), '_empty'): + if query not in self.local_bool_values: raise ParseException(_('Invalid boolean query "{0}"').format(query)) for id_ in candidates: item = self._data[id_] @@ -608,20 +632,20 @@ class ResultCache(SearchQueryParser): # {{{ val = force_to_bool(item[loc]) if not bools_are_tristate: if val is None or not val: # item is None or set to false - if query in [_('no'), _('unchecked'), '_no', 'false']: + if query in (self.local_no, self.local_unchecked, '_no', 'false'): matches.add(item[0]) else: # item is explicitly set to true - if query in [_('yes'), _('checked'), '_yes', 'true']: + if query in (self.local_yes, self.local_checked, '_yes', 'true'): matches.add(item[0]) else: if val is None: - if query in [_('empty'), _('blank'), '_empty', 'false']: + if query in (self.local_empty, self.local_blank, '_empty', 'false'): matches.add(item[0]) elif not val: # is not None and false - if query in [_('no'), _('unchecked'), '_no', 'true']: + if query in (self.local_no, self.local_unchecked, '_no', 'true'): matches.add(item[0]) else: # item is not None and true - if query in [_('yes'), _('checked'), '_yes', 'true']: + if query in (self.local_yes, self.local_checked, '_yes', 'true'): matches.add(item[0]) return matches From abbc24e9c1e987229f591a722bc8e356e00ff755 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 25 Jul 2012 08:58:08 +0200 Subject: [PATCH 25/48] Properly handle the default argument in get_option when the requested plugin does not exist. --- src/calibre/gui2/device.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 8364e06f0f..f08f87bf80 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -539,7 +539,7 @@ class DeviceManager(Thread): # {{{ return self.dynamic_plugin_responses.get() except: traceback.print_exc() - return None + return kwargs.get('default', None) # The dynamic plugin methods below must be called on the GUI thread. They # will switch to the device thread before calling the plugin. From 9c63dab390521999d8afb55c55168d0eba53d41d Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Wed, 25 Jul 2012 11:29:33 +0200 Subject: [PATCH 26/48] Use a synchronizing decorator for the dynamic control methods. --- src/calibre/devices/interface.py | 25 ++++++++++++++------- src/calibre/gui2/device.py | 38 +++++++++----------------------- 2 files changed, 28 insertions(+), 35 deletions(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index 1466732169..c9e62bcfca 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -514,8 +514,11 @@ class DevicePlugin(Plugin): ''' pass - # Dynamic control interface - # All of these methods are called on the device_manager thread + # Dynamic control interface. + # The following methods are probably called on the GUI thread. Any driver + # that implements these methods must take pains to be thread safe, because + # the device_manager might be using the driver at the same time that one of + # these methods is called. def is_dynamically_controllable(self): ''' @@ -523,7 +526,8 @@ class DevicePlugin(Plugin): a string, then a) it supports the device manager's dynamic control interface, and b) that name is to be used when talking to the plugin. - This method must be called from the device_manager thread. + This method can be called on the GUI thread. A driver that implements + this method must be thread safe. ''' return None @@ -533,7 +537,8 @@ class DevicePlugin(Plugin): to accept device connections however it does that. If the plugin is already accepting connections, then do nothing. - This method must be called from the device_manager thread. + This method can be called on the GUI thread. A driver that implements + this method must be thread safe. ''' pass @@ -544,7 +549,8 @@ class DevicePlugin(Plugin): this method should call shutdown. If the plugin is already not accepting connections, then do nothing. - This method must be called from the device_manager thread. + This method can be called on the GUI thread. A driver that implements + this method must be thread safe. ''' pass @@ -554,7 +560,8 @@ class DevicePlugin(Plugin): be called when the plugin is not started. Return None if the option does not exist. - This method must be called from the device_manager thread. + This method can be called on the GUI thread. A driver that implements + this method must be thread safe. ''' return default @@ -563,7 +570,8 @@ class DevicePlugin(Plugin): Set the value of the option indicated by opt_string. This method can be called when the plugin is not started. - This method must be called from the device_manager thread. + This method can be called on the GUI thread. A driver that implements + this method must be thread safe. ''' pass @@ -571,7 +579,8 @@ class DevicePlugin(Plugin): ''' Return True if the plugin is started, otherwise false - This method must be called from the device_manager thread. + This method can be called on the GUI thread. A driver that implements + this method must be thread safe. ''' return False diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index f08f87bf80..d4b8aa0e9b 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -148,8 +148,6 @@ class DeviceManager(Thread): # {{{ self.call_shutdown_on_disconnect = False self.devices_initialized = Event() self.dynamic_plugins = {} - self.dynamic_plugin_requests = Queue.Queue(0) - self.dynamic_plugin_responses = Queue.Queue(0) def report_progress(self, *args): pass @@ -329,15 +327,8 @@ class DeviceManager(Thread): # {{{ self.current_job = None else: break - while True: - dynamic_method = None - try: - (dynamic_method, args, kwargs) = \ - self.dynamic_plugin_requests.get(timeout=self.sleep_time) - res = dynamic_method(*args, **kwargs) - self.dynamic_plugin_responses.put(res) - except Queue.Empty: - break + time.sleep(self.sleep_time) + # We are exiting. Call the shutdown method for each plugin for p in self.devices: try: @@ -528,36 +519,29 @@ class DeviceManager(Thread): # {{{ # dynamic plugin interface # This is a helper function that handles queueing with the device manager - def _queue_request(self, name, method, *args, **kwargs): - if not is_gui_thread(): - raise ValueError( - 'The device_manager dynamic plugin methods must be called from the GUI thread') - try: - d = self.dynamic_plugins.get(name, None) - if d: - self.dynamic_plugin_requests.put((getattr(d, method), args, kwargs)) - return self.dynamic_plugin_responses.get() - except: - traceback.print_exc() + def _call_request(self, name, method, *args, **kwargs): + d = self.dynamic_plugins.get(name, None) + if d: + return getattr(d, method)(*args, **kwargs) return kwargs.get('default', None) # The dynamic plugin methods below must be called on the GUI thread. They # will switch to the device thread before calling the plugin. def start_plugin(self, name): - self._queue_request(name, 'start_plugin') + self._call_request(name, 'start_plugin') def stop_plugin(self, name): - self._queue_request(name, 'stop_plugin') + self._call_request(name, 'stop_plugin') def get_option(self, name, opt_string, default=None): - return self._queue_request(name, 'get_option', opt_string, default=default) + return self._call_request(name, 'get_option', opt_string, default=default) def set_option(self, name, opt_string, opt_value): - self._queue_request(name, 'set_option', opt_string, opt_value) + self._call_request(name, 'set_option', opt_string, opt_value) def is_running(self, name): - if self._queue_request(name, 'is_running'): + if self._call_request(name, 'is_running'): return True return False From 382248eff11e10670ee59ec3cd03f655e00a5690 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 25 Jul 2012 15:27:07 +0530 Subject: [PATCH 27/48] ... --- src/calibre/devices/android/driver.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index beee1082fb..e0d8cea8e6 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -10,7 +10,7 @@ import cStringIO from calibre.devices.usbms.driver import USBMS -HTC_BCDS = [0x100, 0x0222, 0x0226, 0x227, 0x228, 0x229] +HTC_BCDS = [0x100, 0x0222, 0x0226, 0x227, 0x228, 0x229, 0x9999] class ANDROID(USBMS): @@ -41,9 +41,10 @@ class ANDROID(USBMS): 0xca9 : HTC_BCDS, 0xcac : HTC_BCDS, 0xccf : HTC_BCDS, + 0xcd6 : HTC_BCDS, 0xce5 : HTC_BCDS, 0x2910 : HTC_BCDS, - 0xff9 : HTC_BCDS + [0x9999], + 0xff9 : HTC_BCDS, }, # Eken From cf06ad7e67adcba458cb141f46c850d2b341a70d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 26 Jul 2012 00:20:12 +0530 Subject: [PATCH 28/48] Only write to gui.json once during shutdown --- src/calibre/gui2/ui.py | 7 ++++--- src/calibre/utils/config.py | 9 +++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index b414ef04dd..7a05810f2d 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -702,9 +702,10 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ self.read_layout_settings() def write_settings(self): - config.set('main_window_geometry', self.saveGeometry()) - dynamic.set('sort_history', self.library_view.model().sort_history) - self.save_layout_state() + with gprefs: # Only write to gprefs once + config.set('main_window_geometry', self.saveGeometry()) + dynamic.set('sort_history', self.library_view.model().sort_history) + self.save_layout_state() def quit(self, checked=True, restart=False, debug_on_restart=False, confirm_quit=True): diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py index 3bd6b2c364..734be6c1a4 100644 --- a/src/calibre/utils/config.py +++ b/src/calibre/utils/config.py @@ -240,6 +240,7 @@ class XMLConfig(dict): def __init__(self, rel_path_to_cf_file): dict.__init__(self) + self.no_commit = False self.defaults = {} self.file_path = os.path.join(config_dir, *(rel_path_to_cf_file.split('/'))) @@ -304,6 +305,7 @@ class XMLConfig(dict): self.commit() def commit(self): + if self.no_commit: return if hasattr(self, 'file_path') and self.file_path: dpath = os.path.dirname(self.file_path) if not os.path.exists(dpath): @@ -314,6 +316,13 @@ class XMLConfig(dict): f.truncate() f.write(raw) + def __enter__(self): + self.no_commit = True + + def __exit__(self, *args): + self.no_commit = False + self.commit() + def to_json(obj): if isinstance(obj, bytearray): return {'__class__': 'bytearray', From 1fac69c87032da2afffa17992b8ae9cbcb9df4db Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 26 Jul 2012 10:06:45 +0530 Subject: [PATCH 29/48] ... --- manual/develop.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/manual/develop.rst b/manual/develop.rst index 12bbcefe57..ce8b02a70d 100755 --- a/manual/develop.rst +++ b/manual/develop.rst @@ -150,9 +150,8 @@ the previously checked out |app| code directory, for example:: calibre is the directory that contains the src and resources sub-directories. Ensure you have installed the |app| commandline tools via :guilabel:`Preferences->Advanced->Miscellaneous` in the |app| GUI. The next step is to set the environment variable ``CALIBRE_DEVELOP_FROM`` to the absolute path of the src directory. -So, following the example above, it would be ``/Users/kovid/work/calibre/src``. Apple -`documentation `_ -on how to set environment variables. +So, following the example above, it would be ``/Users/kovid/work/calibre/src``. +`How to set environment variables `_. Once you have set the environment variable, open a new Terminal and check that it was correctly set by using the command:: From fff1892577c90561a92e6d3f27646df3266e6587 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 26 Jul 2012 10:14:12 +0530 Subject: [PATCH 30/48] Updated The Sun --- recipes/the_sun.recipe | 46 ++++++++++++++++++++---------------------- 1 file changed, 22 insertions(+), 24 deletions(-) diff --git a/recipes/the_sun.recipe b/recipes/the_sun.recipe index ae7c599328..d93ac2c49b 100644 --- a/recipes/the_sun.recipe +++ b/recipes/the_sun.recipe @@ -1,4 +1,4 @@ -import re, random +import random from calibre import browser from calibre.web.feeds.recipes import BasicNewsRecipe @@ -8,45 +8,43 @@ class AdvancedUserRecipe1325006965(BasicNewsRecipe): title = u'The Sun UK' description = 'Articles from The Sun tabloid UK' __author__ = 'Dave Asbury' - # last updated 15/7/12 + # last updated 25/7/12 language = 'en_GB' oldest_article = 1 - max_articles_per_feed = 15 + max_articles_per_feed = 12 remove_empty_feeds = True no_stylesheets = True masthead_url = 'http://www.thesun.co.uk/sol/img/global/Sun-logo.gif' encoding = 'UTF-8' - - remove_empty_feeds = True remove_javascript = True no_stylesheets = True + + + #preprocess_regexps = [ + # (re.compile(r'