diff --git a/resources/recipes/new_scientist.recipe b/resources/recipes/new_scientist.recipe index 02bbbe4d42..434c41f525 100644 --- a/resources/recipes/new_scientist.recipe +++ b/resources/recipes/new_scientist.recipe @@ -5,6 +5,7 @@ newscientist.com ''' import re +import urllib from calibre.web.feeds.news import BasicNewsRecipe class NewScientist(BasicNewsRecipe): @@ -24,7 +25,7 @@ class NewScientist(BasicNewsRecipe): needs_subscription = 'optional' extra_css = """ body{font-family: Arial,sans-serif} - img{margin-bottom: 0.8em} + img{margin-bottom: 0.8em; display: block} .quotebx{font-size: x-large; font-weight: bold; margin-right: 2em; margin-left: 2em} """ @@ -41,12 +42,14 @@ class NewScientist(BasicNewsRecipe): def get_browser(self): br = BasicNewsRecipe.get_browser() br.open('http://www.newscientist.com/') - if self.username is not None and self.password is not None: - br.open('https://www.newscientist.com/user/login?redirectURL=') - br.select_form(nr=2) - br['loginId' ] = self.username - br['password'] = self.password - br.submit() + if self.username is not None and self.password is not None: + br.open('https://www.newscientist.com/user/login') + data = urllib.urlencode({ 'source':'form' + ,'redirectURL':'' + ,'loginId':self.username + ,'password':self.password + }) + br.open('https://www.newscientist.com/user/login',data) return br remove_tags = [ @@ -55,21 +58,22 @@ class NewScientist(BasicNewsRecipe): ,dict(name='p' , attrs={'class':['marker','infotext' ]}) ,dict(name='meta' , attrs={'name' :'description' }) ,dict(name='a' , attrs={'rel' :'tag' }) + ,dict(name='ul' , attrs={'class':'markerlist' }) ,dict(name=['link','base','meta','iframe','object','embed']) ] remove_tags_after = dict(attrs={'class':['nbpcopy','comments']}) - remove_attributes = ['height','width','lang'] + remove_attributes = ['height','width','lang','onclick'] feeds = [ - (u'Latest Headlines' , u'http://feeds.newscientist.com/science-news' ) - ,(u'Magazine' , u'http://www.newscientist.com/feed/magazine' ) - ,(u'Health' , u'http://www.newscientist.com/feed/view?id=2&type=channel' ) - ,(u'Life' , u'http://www.newscientist.com/feed/view?id=3&type=channel' ) - ,(u'Space' , u'http://www.newscientist.com/feed/view?id=6&type=channel' ) - ,(u'Physics and Mathematics' , u'http://www.newscientist.com/feed/view?id=4&type=channel' ) - ,(u'Environment' , u'http://www.newscientist.com/feed/view?id=1&type=channel' ) - ,(u'Science in Society' , u'http://www.newscientist.com/feed/view?id=5&type=channel' ) - ,(u'Tech' , u'http://www.newscientist.com/feed/view?id=7&type=channel' ) + (u'Latest Headlines' , u'http://feeds.newscientist.com/science-news' ) + ,(u'Magazine' , u'http://feeds.newscientist.com/magazine' ) + ,(u'Health' , u'http://feeds.newscientist.com/health' ) + ,(u'Life' , u'http://feeds.newscientist.com/life' ) + ,(u'Space' , u'http://feeds.newscientist.com/space' ) + ,(u'Physics and Mathematics' , u'http://feeds.newscientist.com/physics-math' ) + ,(u'Environment' , u'http://feeds.newscientist.com/environment' ) + ,(u'Science in Society' , u'http://feeds.newscientist.com/science-in-society' ) + ,(u'Tech' , u'http://feeds.newscientist.com/tech' ) ] def get_article_url(self, article): @@ -79,11 +83,21 @@ class NewScientist(BasicNewsRecipe): return url + '?full=true&print=true' def preprocess_html(self, soup): + if soup.html.has_key('id'): + del soup.html['id'] + for item in soup.findAll(style=True): + del item['style'] for item in soup.findAll(['quote','quotetext']): item.name='p' + for item in soup.findAll(['xref','figref']): + tstr = item.string + item.replaceWith(tstr) for tg in soup.findAll('a'): if tg.string == 'Home': tg.parent.extract() - return self.adeify_images(soup) - return self.adeify_images(soup) + else: + if tg.string is not None: + tstr = tg.string + tg.replaceWith(tstr) + return soup diff --git a/resources/recipes/radikal_tr.recipe b/resources/recipes/radikal_tr.recipe index 2d71c238dd..18021f1bb4 100644 --- a/resources/recipes/radikal_tr.recipe +++ b/resources/recipes/radikal_tr.recipe @@ -13,14 +13,16 @@ class Radikal_tr(BasicNewsRecipe): description = 'News from Turkey' publisher = 'radikal' category = 'news, politics, Turkey' - oldest_article = 2 + oldest_article = 7 max_articles_per_feed = 150 no_stylesheets = True encoding = 'cp1254' use_embedded_content = False masthead_url = 'http://www.radikal.com.tr/D/i/1/V2/radikal_logo.jpg' language = 'tr' - extra_css = ' @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} .article_description,body{font-family: Arial,Verdana,Helvetica,sans1,sans-serif } ' + extra_css = """ @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} + .article_description,body{font-family: Arial,Verdana,Helvetica,sans1,sans-serif} + """ conversion_options = { 'comment' : description @@ -34,7 +36,13 @@ class Radikal_tr(BasicNewsRecipe): remove_tags_after = dict(attrs={'id':'haberDetayYazi'}) - feeds = [(u'Yazarlar', u'http://www.radikal.com.tr/d/rss/RssYazarlar.xml')] + feeds = [ + (u'Yazarlar' , u'http://www.radikal.com.tr/d/rss/RssYazarlar.xml') + ,(u'Turkiye' , u'http://www.radikal.com.tr/d/rss/Rss_97.xml' ) + ,(u'Politika' , u'http://www.radikal.com.tr/d/rss/Rss_98.xml' ) + ,(u'Dis Haberler', u'http://www.radikal.com.tr/d/rss/Rss_100.xml' ) + ,(u'Ekonomi' , u'http://www.radikal.com.tr/d/rss/Rss_101.xml' ) + ] def print_version(self, url): articleid = url.rpartition('ArticleID=')[2] diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 5f3aab142e..793c1fa0de 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -457,7 +457,8 @@ from calibre.devices.blackberry.driver import BLACKBERRY from calibre.devices.cybook.driver import CYBOOK, ORIZON from calibre.devices.eb600.driver import EB600, COOL_ER, SHINEBOOK, \ POCKETBOOK360, GER2, ITALICA, ECLICTO, DBOOK, INVESBOOK, \ - BOOQ, ELONEX, POCKETBOOK301, MENTOR, POCKETBOOK602 + BOOQ, ELONEX, POCKETBOOK301, MENTOR, POCKETBOOK602, \ + POCKETBOOK701 from calibre.devices.iliad.driver import ILIAD from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800 from calibre.devices.jetbook.driver import JETBOOK, MIBUK, JETBOOK_MINI @@ -545,9 +546,7 @@ plugins += [ JETBOOK_MINI, MIBUK, SHINEBOOK, - POCKETBOOK360, - POCKETBOOK301, - POCKETBOOK602, + POCKETBOOK360, POCKETBOOK301, POCKETBOOK602, POCKETBOOK701, KINDLE, KINDLE2, KINDLE_DX, diff --git a/src/calibre/devices/eb600/driver.py b/src/calibre/devices/eb600/driver.py index 54d73d9c1d..bc8b87533c 100644 --- a/src/calibre/devices/eb600/driver.py +++ b/src/calibre/devices/eb600/driver.py @@ -246,3 +246,23 @@ class POCKETBOOK602(USBMS): VENDOR_NAME = '' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['PB602', 'PB902'] +class POCKETBOOK701(USBMS): + + name = 'PocketBook 701 Device Interface' + description = _('Communicate with the PocketBook 701') + author = _('Kovid Goyal') + + supported_platforms = ['windows', 'osx', 'linux'] + FORMATS = ['epub', 'fb2', 'prc', 'mobi', 'pdf', 'djvu', 'rtf', 'chm', + 'doc', 'tcr', 'txt'] + + EBOOK_DIR_MAIN = 'books' + SUPPORTS_SUB_DIRS = True + + VENDOR_ID = [0x18d1] + PRODUCT_ID = [0xa004] + BCD = [0x0224] + + VENDOR_NAME = 'ANDROID' + WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE' + diff --git a/src/calibre/devices/sne/driver.py b/src/calibre/devices/sne/driver.py index 0ccac13245..bb8d34c59c 100644 --- a/src/calibre/devices/sne/driver.py +++ b/src/calibre/devices/sne/driver.py @@ -23,16 +23,16 @@ class SNE(USBMS): FORMATS = ['epub', 'pdf', 'txt'] VENDOR_ID = [0x04e8] - PRODUCT_ID = [0x2051, 0x2053] + PRODUCT_ID = [0x2051, 0x2053, 0x2054] BCD = [0x0323] VENDOR_NAME = 'SAMSUNG' - WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'SNE-60' + WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['SNE-60', 'E65'] MAIN_MEMORY_VOLUME_LABEL = 'SNE Main Memory' STORAGE_CARD_VOLUME_LABEL = 'SNE Storage Card' - EBOOK_DIR_MAIN = 'Books' + EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'Books' SUPPORTS_SUB_DIRS = True diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py index 9bdf937dd1..da4d1178eb 100644 --- a/src/calibre/ebooks/__init__.py +++ b/src/calibre/ebooks/__init__.py @@ -22,6 +22,9 @@ class UnknownFormatError(Exception): class DRMError(ValueError): pass +class ParserError(ValueError): + pass + BOOK_EXTENSIONS = ['lrf', 'rar', 'zip', 'rtf', 'lit', 'txt', 'htm', 'xhtm', 'html', 'xhtml', 'pdf', 'pdb', 'pdr', 'prc', 'mobi', 'azw', 'doc', 'epub', 'fb2', 'djvu', 'lrx', 'cbr', 'cbz', 'cbc', 'oebzip', @@ -39,6 +42,10 @@ class HTMLRenderer(object): try: if not ok: raise RuntimeError('Rendering of HTML failed.') + de = self.page.mainFrame().documentElement() + pe = de.findFirst('parsererror') + if not pe.isNull(): + raise ParserError(pe.toPlainText()) image = QImage(self.page.viewportSize(), QImage.Format_ARGB32) image.setDotsPerMeterX(96*(100/2.54)) image.setDotsPerMeterY(96*(100/2.54)) @@ -104,7 +111,7 @@ def render_html_svg_workaround(path_to_html, log, width=590, height=750): return data -def render_html(path_to_html, width=590, height=750): +def render_html(path_to_html, width=590, height=750, as_xhtml=True): from PyQt4.QtWebKit import QWebPage from PyQt4.Qt import QEventLoop, QPalette, Qt, SIGNAL, QUrl, QSize from calibre.gui2 import is_ok_to_use_qt @@ -122,11 +129,18 @@ def render_html(path_to_html, width=590, height=750): renderer = HTMLRenderer(page, loop) page.connect(page, SIGNAL('loadFinished(bool)'), renderer, Qt.QueuedConnection) - page.mainFrame().load(QUrl.fromLocalFile(path_to_html)) + if as_xhtml: + page.mainFrame().setContent(open(path_to_html, 'rb').read(), + 'application/xhtml+xml', QUrl.fromLocalFile(path_to_html)) + else: + page.mainFrame().load(QUrl.fromLocalFile(path_to_html)) loop.exec_() renderer.loop = renderer.page = None del page del loop + if isinstance(renderer.exception, ParserError) and as_xhtml: + return render_html(path_to_html, width=width, height=height, + as_xhtml=False) return renderer def check_ebook_format(stream, current_guess): diff --git a/src/calibre/ebooks/metadata/pdf.py b/src/calibre/ebooks/metadata/pdf.py index 2d1935539e..20a4c4659e 100644 --- a/src/calibre/ebooks/metadata/pdf.py +++ b/src/calibre/ebooks/metadata/pdf.py @@ -17,6 +17,7 @@ pdfreflow, pdfreflow_error = plugins['pdfreflow'] def get_metadata(stream, cover=True): if pdfreflow is None: raise RuntimeError(pdfreflow_error) + stream.seek(0) raw = stream.read() #isbn = _isbn_pat.search(raw) #if isbn is not None: diff --git a/src/calibre/ebooks/oeb/stylizer.py b/src/calibre/ebooks/oeb/stylizer.py index 616cd3b800..40b82514c1 100644 --- a/src/calibre/ebooks/oeb/stylizer.py +++ b/src/calibre/ebooks/oeb/stylizer.py @@ -205,7 +205,10 @@ class Stylizer(object): NameError, # thrown on OS X instead of SelectorSyntaxError SelectorSyntaxError): continue - matches = selector(tree) + try: + matches = selector(tree) + except etree.XPathEvalError: + continue if not matches: ntext = capital_sel_pat.sub(lambda m: m.group().lower(), text) diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 92b5932406..3b071aa024 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -593,7 +593,6 @@ class DeviceMenu(QMenu): # {{{ # }}} - class DeviceMixin(object): # {{{ def __init__(self): diff --git a/src/calibre/gui2/viewer/config.ui b/src/calibre/gui2/viewer/config.ui index 4066daada2..6e37170154 100644 --- a/src/calibre/gui2/viewer/config.ui +++ b/src/calibre/gui2/viewer/config.ui @@ -7,14 +7,14 @@ 0 0 479 - 606 + 591 Configure Ebook viewer - + :/images/config.png:/images/config.png @@ -85,11 +85,7 @@ - - - - - + &Default font size: @@ -99,7 +95,7 @@ - + px @@ -112,7 +108,7 @@ - + Monospace &font size: @@ -122,7 +118,7 @@ - + px @@ -135,7 +131,7 @@ - + S&tandard font: @@ -145,7 +141,7 @@ - + @@ -164,91 +160,125 @@ - - - - Remember last used &window size - - - - - - - Remember the &current page when quitting - - - - - - - px - - - 100 - - - 10000 - - - - - - - Maximum &view width: - - - max_view_width - - - - - - - H&yphenate (break line in the middle of large words) - - - - - - - The default language to use for hyphenation rules. If the book does not specify a language, this will be used. - - - - - - - Default &language for hyphenation: - - - hyphenate_default_lang - - - - - - - &Resize images larger than the viewer window (needs restart) - - - - - - - &User stylesheet - - - - - - - - + + + + + + Remember last used &window size + + + + + + + Remember the &current page when quitting + + + + + + + H&yphenate (break line in the middle of large words) + + + + + + + The default language to use for hyphenation rules. If the book does not specify a language, this will be used. + + + + + + + Default &language for hyphenation: + + + hyphenate_default_lang + + + + + + + &Resize images larger than the viewer window (needs restart) + + + + + + + Page flip &duration: + + + opt_page_flip_duration + + + + + + + disabled + + + secs + + + 1 + + + 0.100000000000000 + + + 3.000000000000000 + + + 0.100000000000000 + + + 0.500000000000000 + + + + + + + Mouse &wheel flips pages + + + + + + + px + + + 100 + + + 10000 + + + + + + + Maximum &view width: + + + max_view_width + + + + + @@ -268,6 +298,29 @@ + + + User &Stylesheet + + + + + + <p>A CSS stylesheet that can be used to control the look and feel of books. For examples, click <a href="http://www.mobileread.com/forums/showthread.php?t=51500">here</a>. + + + true + + + true + + + + + + + + @@ -276,12 +329,8 @@ serif_family sans_family mono_family - default_font_size - mono_font_size - standard_font max_view_width opt_remember_window_size - css buttonBox diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index afaea41bc6..f131dd522d 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -18,6 +18,7 @@ from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings from calibre.utils.config import Config, StringConfig from calibre.utils.localization import get_language from calibre.gui2.viewer.config_ui import Ui_Dialog +from calibre.gui2.viewer.flip import SlideFlip from calibre.gui2.shortcuts import Shortcuts, ShortcutConfig from calibre.constants import iswindows from calibre import prints, guess_type @@ -52,6 +53,11 @@ def config(defaults=None): help=_('Default language for hyphenation rules')) c.add_opt('remember_current_page', default=True, help=_('Save the current position in the document, when quitting')) + c.add_opt('wheel_flips_pages', default=False, + help=_('Have the mouse wheel turn pages')) + c.add_opt('page_flip_duration', default=0.5, + help=_('The time, in seconds, for the page flip animation. Default' + ' is half a second.')) fonts = c.add_group('FONTS', _('Font options')) fonts('serif_family', default='Times New Roman' if iswindows else 'Liberation Serif', @@ -75,6 +81,8 @@ class ConfigDialog(QDialog, Ui_Dialog): opts = config().parse() self.opt_remember_window_size.setChecked(opts.remember_window_size) self.opt_remember_current_page.setChecked(opts.remember_current_page) + self.opt_wheel_flips_pages.setChecked(opts.wheel_flips_pages) + self.opt_page_flip_duration.setValue(opts.page_flip_duration) self.serif_family.setCurrentFont(QFont(opts.serif_family)) self.sans_family.setCurrentFont(QFont(opts.sans_family)) self.mono_family.setCurrentFont(QFont(opts.mono_family)) @@ -122,6 +130,8 @@ class ConfigDialog(QDialog, Ui_Dialog): c.set('max_view_width', int(self.max_view_width.value())) c.set('hyphenate', self.hyphenate.isChecked()) c.set('remember_current_page', self.opt_remember_current_page.isChecked()) + c.set('wheel_flips_pages', self.opt_wheel_flips_pages.isChecked()) + c.set('page_flip_duration', self.opt_page_flip_duration.value()) idx = self.hyphenate_default_lang.currentIndex() c.set('hyphenate_default_lang', str(self.hyphenate_default_lang.itemData(idx).toString())) @@ -197,6 +207,9 @@ class Document(QWebPage): self.hyphenate = opts.hyphenate self.hyphenate_default_lang = opts.hyphenate_default_lang self.do_fit_images = opts.fit_images + self.page_flip_duration = opts.page_flip_duration + self.enable_page_flip = self.page_flip_duration > 0.1 + self.wheel_flips_pages = opts.wheel_flips_pages def fit_images(self): if self.do_fit_images: @@ -453,6 +466,8 @@ class DocumentView(QWebView): def __init__(self, *args): QWebView.__init__(self, *args) + self.flipper = SlideFlip(self) + self.is_auto_repeat_event = False self.debug_javascript = False self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer') self.self_closing_pat = re.compile(r'<([a-z1-6]+)\s+([^>]+)/>', @@ -693,6 +708,13 @@ class DocumentView(QWebView): self.manager.scrolled(self.document.scroll_fraction) self.turn_off_internal_scrollbars() + if self.flipper.isVisible(): + if self.flipper.running: + self.flipper.setVisible(False) + else: + self.flipper(self.current_page_image(), + duration=self.document.page_flip_duration) + def turn_off_internal_scrollbars(self): self.document.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) @@ -708,12 +730,17 @@ class DocumentView(QWebView): return False return True - def find_next_blank_line(self, overlap): + def current_page_image(self, overlap=-1): + if overlap < 0: + overlap = self.height() img = QImage(self.width(), overlap, QImage.Format_ARGB32) painter = QPainter(img) - # Render a region of width x overlap pixels atthe bottom of the current viewport self.document.mainFrame().render(painter, QRegion(0, 0, self.width(), overlap)) painter.end() + return img + + def find_next_blank_line(self, overlap): + img = self.current_page_image(overlap) for i in range(overlap-1, -1, -1): if self.test_line(img, i): self.scroll_by(y=i, notify=False) @@ -721,22 +748,42 @@ class DocumentView(QWebView): self.scroll_by(y=overlap) def previous_page(self): + if self.flipper.running and not self.is_auto_repeat_event: + return + if self.loading_url is not None: + return + epf = self.document.enable_page_flip and not self.is_auto_repeat_event + delta_y = self.document.window_height - 25 if self.document.at_top: if self.manager is not None: self.to_bottom = True - self.manager.previous_document() + if epf: + self.flipper.initialize(self.current_page_image(), False) + self.manager.previous_document() else: opos = self.document.ypos upper_limit = opos - delta_y if upper_limit < 0: upper_limit = 0 if upper_limit < opos: + if epf: + self.flipper.initialize(self.current_page_image(), + forwards=False) self.document.scroll_to(self.document.xpos, upper_limit) + if epf: + self.flipper(self.current_page_image(), + duration=self.document.page_flip_duration) if self.manager is not None: self.manager.scrolled(self.scroll_fraction) def next_page(self): + if self.flipper.running and not self.is_auto_repeat_event: + return + if self.loading_url is not None: + return + epf = self.document.enable_page_flip and not self.is_auto_repeat_event + window_height = self.document.window_height document_height = self.document.height ddelta = document_height - window_height @@ -746,6 +793,8 @@ class DocumentView(QWebView): delta_y = window_height - 25 if self.document.at_bottom or ddelta <= 0: if self.manager is not None: + if epf: + self.flipper.initialize(self.current_page_image()) self.manager.next_document() elif ddelta < 25: self.scroll_by(y=ddelta) @@ -758,6 +807,8 @@ class DocumentView(QWebView): #print 'After set padding=0:', self.document.ypos if opos < oopos: if self.manager is not None: + if epf: + self.flipper.initialize(self.current_page_image()) self.manager.next_document() return lower_limit = opos + delta_y # Max value of top y co-ord after scrolling @@ -766,10 +817,14 @@ class DocumentView(QWebView): padding = lower_limit - max_y if padding == window_height: if self.manager is not None: + if epf: + self.flipper.initialize(self.current_page_image()) self.manager.next_document() return #print 'Setting padding to:', lower_limit - max_y self.document.set_bottom_padding(lower_limit - max_y) + if epf: + self.flipper.initialize(self.current_page_image()) #print 'Document height:', self.document.height max_y = self.document.height - window_height lower_limit = min(max_y, lower_limit) @@ -780,6 +835,9 @@ class DocumentView(QWebView): #print 'After scroll pos:', self.document.ypos self.find_next_blank_line(window_height - actually_scrolled) #print 'After blank line pos:', self.document.ypos + if epf: + self.flipper(self.current_page_image(), + duration=self.document.page_flip_duration) if self.manager is not None: self.manager.scrolled(self.scroll_fraction) #print 'After all:', self.document.ypos @@ -833,6 +891,10 @@ class DocumentView(QWebView): def wheelEvent(self, event): if event.delta() < -14: + if self.document.wheel_flips_pages: + self.next_page() + event.accept() + return if self.document.at_bottom: self.scroll_by(y=15) # at_bottom can lie on windows if self.manager is not None: @@ -840,6 +902,11 @@ class DocumentView(QWebView): event.accept() return elif event.delta() > 14: + if self.document.wheel_flips_pages: + self.previous_page() + event.accept() + return + if self.document.at_top: if self.manager is not None: self.manager.previous_document() @@ -862,7 +929,11 @@ class DocumentView(QWebView): key = self.shortcuts.get_match(event) func = self.goto_location_actions.get(key, None) if func is not None: - func() + self.is_auto_repeat_event = event.isAutoRepeat() + try: + func() + finally: + self.is_auto_repeat_event = False elif key == 'Down': self.scroll_by(y=15) elif key == 'Up': diff --git a/src/calibre/gui2/viewer/flip.py b/src/calibre/gui2/viewer/flip.py new file mode 100644 index 0000000000..5432909b2b --- /dev/null +++ b/src/calibre/gui2/viewer/flip.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import QWidget, QPainter, QPropertyAnimation, QEasingCurve, \ + QRect, QPixmap, Qt, pyqtProperty + +class SlideFlip(QWidget): + + # API {{{ + + # In addition the isVisible() and setVisible() methods must be present + + def __init__(self, parent): + QWidget.__init__(self, parent) + + self.setGeometry(0, 0, 1, 1) + self._current_width = 0 + self.before_image = self.after_image = None + self.animation = QPropertyAnimation(self, 'current_width', self) + self.setVisible(False) + self.animation.valueChanged.connect(self.update) + self.animation.finished.connect(self.finished) + self.flip_forwards = True + self.setAttribute(Qt.WA_OpaquePaintEvent) + + @property + def running(self): + 'True iff animation is currently running' + return self.animation.state() == self.animation.Running + + def initialize(self, image, forwards=True): + ''' + Initialize the flipper, causes the flipper to show itself displaying + the full `image`. + + :param image: The image to display as background + :param forwards: If True flipper will flip forwards, otherwise + backwards + + ''' + self.flip_forwards = forwards + self.before_image = QPixmap.fromImage(image) + self.after_image = None + self.setGeometry(0, 0, image.width(), image.height()) + self.setVisible(True) + + def __call__(self, image, duration=0.5): + ''' + Start the animation. You must have called :meth:`initialize` first. + + :param duration: Animation duration in seconds. + + ''' + if self.running: + return + self.after_image = QPixmap.fromImage(image) + + if self.flip_forwards: + self.animation.setStartValue(image.width()) + self.animation.setEndValue(0) + t = self.before_image + self.before_image = self.after_image + self.after_image = t + self.animation.setEasingCurve(QEasingCurve(QEasingCurve.InExpo)) + else: + self.animation.setStartValue(0) + self.animation.setEndValue(image.width()) + self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo)) + + self.animation.setDuration(duration * 1000) + self.animation.start() + + # }}} + + def finished(self): + self.setVisible(False) + self.before_image = self.after_image = None + + def paintEvent(self, ev): + if self.before_image is None: + return + canvas_size = self.rect() + p = QPainter(self) + p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform) + + p.drawPixmap(canvas_size, self.before_image, + self.before_image.rect()) + if self.after_image is not None: + width = self._current_width + iw = self.after_image.width() + sh = min(self.after_image.height(), canvas_size.height()) + + if self.flip_forwards: + source = QRect(max(0, iw - width), 0, width, sh) + else: + source = QRect(0, 0, width, sh) + + target = QRect(source) + target.moveLeft(0) + p.drawPixmap(target, self.after_image, source) + + p.end() + + def set_current_width(self, val): + self._current_width = val + + current_width = pyqtProperty('int', + fget=lambda self: self._current_width, + fset=set_current_width + ) + + diff --git a/src/calibre/utils/mem.py b/src/calibre/utils/mem.py index f48aec34c6..1f9bff8d63 100644 --- a/src/calibre/utils/mem.py +++ b/src/calibre/utils/mem.py @@ -5,6 +5,8 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' +import gc + ## {{{ http://code.activestate.com/recipes/286222/ (r1) import os @@ -52,4 +54,19 @@ def stacksize(since=0.0): ## end of http://code.activestate.com/recipes/286222/ }}} +def gc_histogram(): + """Returns per-class counts of existing objects.""" + result = {} + for o in gc.get_objects(): + t = type(o) + count = result.get(t, 0) + result[t] = count + 1 + return result + +def diff_hists(h1, h2): + """Prints differences between two results of gc_histogram().""" + for k in h1: + if h1[k] != h2[k]: + print "%s: %d -> %d (%s%d)" % ( + k, h1[k], h2[k], h2[k] > h1[k] and "+" or "", h2[k] - h1[k])