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 ¤t 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 ¤t 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])