diff --git a/manual/creating_plugins.rst b/manual/creating_plugins.rst index 5963f4a890..f39b60dad9 100644 --- a/manual/creating_plugins.rst +++ b/manual/creating_plugins.rst @@ -195,7 +195,7 @@ It can get tiresome to keep re-adding a plugin to calibre to test small changes. Once you've located the zip file of your plugin you can then directly update it with your changes instead of re-adding it each time. To do so from the command line, in the directory that contains your plugin source code, use:: - calibre -s; sleep 4s; zip -R /path/to/plugin/zip/file.zip *; calibre + calibre -s; zip -R /path/to/plugin/zip/file.zip *; calibre This will shutdown a running calibre. Wait for the shutdown to complete, then update your plugin files and relaunch calibre. It relies on the freely available zip command line tool. diff --git a/recipes/abc_py.recipe b/recipes/abc_py.recipe index 297129d269..41005c6844 100644 --- a/recipes/abc_py.recipe +++ b/recipes/abc_py.recipe @@ -1,5 +1,5 @@ __license__ = 'GPL v3' -__copyright__ = '2010, Darko Miletic ' +__copyright__ = '2010-2012, Darko Miletic ' ''' abc.com.py ''' @@ -7,7 +7,7 @@ abc.com.py from calibre.web.feeds.news import BasicNewsRecipe class ABC_py(BasicNewsRecipe): - title = 'ABC digital' + title = 'ABC Color' __author__ = 'Darko Miletic' description = 'Noticias de Paraguay y el resto del mundo' publisher = 'ABC' @@ -15,12 +15,16 @@ class ABC_py(BasicNewsRecipe): oldest_article = 2 max_articles_per_feed = 200 no_stylesheets = True - encoding = 'cp1252' + encoding = 'utf8' use_embedded_content = False language = 'es_PY' remove_empty_feeds = True + masthead_url = 'http://www.abc.com.py/plantillas/img/abc-logo.png' publication_type = 'newspaper' - extra_css = ' body{font-family: Arial,Helvetica,sans-serif } img{margin-bottom: 0.4em} ' + extra_css = """ + body{font-family: UnitSlabProMedium,"Times New Roman",serif } + img{margin-bottom: 0.4em; display: block;} + """ conversion_options = { 'comment' : description @@ -29,21 +33,19 @@ class ABC_py(BasicNewsRecipe): , 'language' : language } - remove_tags = [dict(name=['form','iframe','embed','object','link','base','table']),dict(attrs={'class':'toolbox'})] - remove_tags_after = dict(attrs={'class':'date'}) - keep_only_tags = [dict(attrs={'class':'zcontent'})] + remove_tags = [ + dict(name=['form','iframe','embed','object','link','base','table']), + dict(attrs={'class':['es-carousel-wrapper']}), + dict(attrs={'id':['tools','article-banner-1']}) + ] + keep_only_tags = [dict(attrs={'id':'article'})] feeds = [ - (u'Ultimo momento' , u'http://www.abc.com.py/ultimo-momento.xml' ) - ,(u'Nacionales' , u'http://www.abc.com.py/nacionales.xml' ) - ,(u'Internacionales' , u'http://www.abc.com.py/internacionales.xml' ) - ,(u'Deportes' , u'http://www.abc.com.py/deportes.xml' ) - ,(u'Espectaculos' , u'http://www.abc.com.py/espectaculos.xml' ) - ,(u'Ciencia y Tecnologia', u'http://www.abc.com.py/ciencia-y-tecnologia.xml') + (u'Ultimo momento', u'http://www.abc.com.py/rss.xml' ) + ,(u'Nacionales' , u'http://www.abc.com.py/nacionales/rss.xml' ) + ,(u'Mundo' , u'http://www.abc.com.py/internacionales/rss.xml') + ,(u'Deportes' , u'http://www.abc.com.py/deportes/rss.xml' ) + ,(u'Espectaculos' , u'http://www.abc.com.py/espectaculos/rss.xml' ) + ,(u'TecnoCiencia' , u'http://www.abc.com.py/ciencia/rss.xml' ) ] - - def preprocess_html(self, soup): - for item in soup.findAll(style=True): - del item['style'] - return soup diff --git a/recipes/bbc_brasil.recipe b/recipes/bbc_brasil.recipe index 4a0fc03d96..a2d83944d1 100644 --- a/recipes/bbc_brasil.recipe +++ b/recipes/bbc_brasil.recipe @@ -147,10 +147,9 @@ class BBCBrasilRecipe(BasicNewsRecipe): # Author of this recipe. - __author__ = 'claviola' + __author__ = 'Carlos Laviola' - # Specify English as the language of the RSS feeds (ISO-639 code). - language = 'en_GB' + language = 'pt_BR' # Set tags. tags = 'news, sport, blog' diff --git a/recipes/ct24.recipe b/recipes/ct24.recipe new file mode 100644 index 0000000000..b9ccd2dd84 --- /dev/null +++ b/recipes/ct24.recipe @@ -0,0 +1,12 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class AdvancedUserRecipe1339974788(BasicNewsRecipe): + title = u'\u010cT24' + oldest_article = 1 + language = 'cs' + __author__ = 'zoidozoido' + max_articles_per_feed = 100 + auto_cleanup = True + + feeds = [(u'Hlavn\xed zpr\xe1vy', u'http://www.ceskatelevize.cz/ct24/rss/hlavni-zpravy/'), (u'Dom\xe1c\xed', u'http://www.ceskatelevize.cz/ct24/rss/domaci/'), (u'Sv\u011bt', u'http://www.ceskatelevize.cz/ct24/rss/svet/'), (u'Regiony', u'http://www.ceskatelevize.cz/ct24/rss/regiony/'), (u'Kultura', u'http://www.ceskatelevize.cz/ct24/rss/kultura/'), (u'Ekonomika', u'http://www.ceskatelevize.cz/ct24/rss/ekonomika/'), (u'Sport - hlavn\xed zpr\xe1vy', u'http://www.ceskatelevize.cz/ct4/rss/hlavni-zpravy/'), (u'OH 2012', u'http://www.ceskatelevize.cz/ct4/rss/oh-2012/')] + remove_tags = [dict(name='img')] diff --git a/recipes/marketing_sensoriale.recipe b/recipes/marketing_sensoriale.recipe new file mode 100644 index 0000000000..214e0ea1b1 --- /dev/null +++ b/recipes/marketing_sensoriale.recipe @@ -0,0 +1,56 @@ +from calibre.web.feeds.news import BasicNewsRecipe +from calibre.utils.ipc.simple_worker import fork_job +from calibre.ptempfile import PersistentTemporaryFile + +js_fetcher = ''' + +import calibre.web.jsbrowser.browser as jsbrowser + +def grab(url): + browser = jsbrowser.Browser() + #10 second timeout + browser.visit(url, 10) + browser.run_for_a_time(10) + html = browser.html + browser.close() + return html + + ''' +class MarketingSensoriale(BasicNewsRecipe): + + title = u'Marketing sensoriale' + __author__ = 'NotTaken' + description = 'Marketing Sensoriale, il Blog' + category = 'Blog' + oldest_article = 7 + max_articles_per_feed = 200 + no_stylesheets = True + encoding = 'utf8' + use_embedded_content = False + language = 'it' + remove_empty_feeds = True + recursions = 0 + requires_version = (0, 8, 58) + auto_cleanup = False + simultaneous_downloads = 1 + articles_are_obfuscated = True + + remove_tags_after = [dict(name='div', attrs={'class':['article-footer']})] + + + def get_article_url(self, article): + return article.get('feedburner_origlink', None) + + def get_obfuscated_article(self, url): + result = fork_job(js_fetcher, 'grab', (url,), module_is_source_code=True) + + html = result['result'] + if isinstance(html, type(u'')): + html = html.encode('utf-8') + pt = PersistentTemporaryFile('.html') + pt.write(html) + pt.close() + return pt.name + + feeds = [(u'Marketing sensoriale', u'http://feeds.feedburner.com/MarketingSensoriale?format=xml')] + diff --git a/recipes/new_statesman.recipe b/recipes/new_statesman.recipe new file mode 100644 index 0000000000..55524d09ff --- /dev/null +++ b/recipes/new_statesman.recipe @@ -0,0 +1,90 @@ +__license__ = 'GPL v3' +''' +newstatesman.com +''' +from calibre.web.feeds.news import BasicNewsRecipe + +class NewStatesman(BasicNewsRecipe): + + title = 'New Statesman' + language = 'en_GB' + __author__ = "NotTaken" + description = "Britain's Current Affairs & Politics Magazine (bi-weekly)" + oldest_article = 4.0 + no_stylesheets = True + use_embedded_content = False + remove_empty_feeds = True + + keep_only_tags = [dict(attrs={'class' : 'node'})] + + remove_tags_after = [ + dict(attrs={'class' : lambda x: x and 'content123' in x}) + ] + + remove_tags = [ + dict(attrs={'class' : lambda x: x and 'links_bookmark' in x}) + ] + + extra_css = ''' + .title-main {font-size: x-large;} + h2 { font-size: small; } + h1 { font-size: medium; } + .field-field-nodeimage-title { + font-size: small; + color: #3C3C3C; + } + .link_col { + font-size: x-small; + } + ''' + + processed_urls = [] + + def populate_article_metadata(self, article, soup, first): + if first and hasattr(self, 'add_toc_thumbnail'): + pic = soup.find('img') + if pic is not None: + self.add_toc_thumbnail(article,pic['src']) + + def get_article_url(self, article): + url = BasicNewsRecipe.get_article_url(self,article) + + if url in self.processed_urls: + self.log('skipping duplicate article: %s' %article.title ) + return None + + self.processed_urls.append(url) + return url + + + feeds = [ + (u'Politics', + u'http://www.newstatesman.com/politics.rss'), + (u'Business', + u'http://www.newstatesman.com/business.rss'), + (u'Economics', + u'http://www.newstatesman.com/economics.rss'), + (u'Culture', + u'http://www.newstatesman.com/culture.rss'), + (u'Media', + u'http://www.newstatesman.com/media.rss'), + (u'Books', + u'http://www.newstatesman.com/taxonomy/term/feed/27'), + (u'Life & Society', + u'http://www.newstatesman.com/taxonomyfeed/11'), + (u'World Affairs', + u'http://www.newstatesman.com/world-affairs.rss'), + (u'Sci-Tech', + u'http://www.newstatesman.com/feeds/topics/sci-tech.rss'), + (u'Others', + u'http://www.newstatesman.com/feeds_allsite/site_feed.php'), + ] + + + + + + + + + diff --git a/recipes/o_globo.recipe b/recipes/o_globo.recipe index 0cf00d874c..2fa0043c0e 100644 --- a/recipes/o_globo.recipe +++ b/recipes/o_globo.recipe @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python __license__ = 'GPL v3' __copyright__ = '2009, Darko Miletic ' @@ -10,11 +10,11 @@ from calibre.web.feeds.news import BasicNewsRecipe class OGlobo(BasicNewsRecipe): title = 'O Globo' - __author__ = 'Darko Miletic and Sujata Raman' + __author__ = 'Darko Miletic and Carlos Laviola' description = 'News from Brasil' publisher = 'O Globo' category = 'news, politics, Brasil' - oldest_article = 2 + oldest_article = 7 max_articles_per_feed = 100 no_stylesheets = True use_embedded_content = False @@ -35,47 +35,39 @@ class OGlobo(BasicNewsRecipe): body{font-family:Arial,Helvetica,sans-serif;font-size:x-small;} h3{font-size:large; color:#082963; font-weight:bold;} #ident{color:#0179B4; font-size:xx-small;} - p{color:#000000;font-weight:normal;} + p{color:#000000;font-weight:normal;} .commentario p{color:#007BB5; font-style:italic;} ''' - - keep_only_tags = [dict(name='div', attrs={'id':'ltintb'}), - dict(name='a', attrs={'class':['img imgLoader','img ftr imgLoader']}),] - remove_tags = [ dict(name='script') - ,dict(name='object') ,dict(name='form') - ,dict(name='div', attrs={'id':['linksPatGoogle','rdpm','cor','com','env','rcm_st','coment',]}) - ,dict(name='div', attrs={'class':'box-zap-anu2'}) - ,dict(name='a', attrs={'class':'assine'}) - ,dict(name='link') + ,dict(name='div', attrs={'id':'header'}) + ,dict(name='p', attrs={'id':'info-date-press'}) ] feeds = [ - (u'Todos os canais', u'http://oglobo.globo.com/rss/plantao.xml') - ,(u'Ciencia', u'http://oglobo.globo.com/rss/plantaociencia.xml') - ,(u'Educacao', u'http://oglobo.globo.com/rss/plantaoeducacao.xml') - ,(u'Opiniao', u'http://oglobo.globo.com/rss/plantaoopiniao.xml') - ,(u'Sao Paulo', u'http://oglobo.globo.com/rss/plantaosaopaulo.xml') - ,(u'Viagem', u'http://oglobo.globo.com/rss/plantaoviagem.xml') - ,(u'Cultura', u'http://oglobo.globo.com/rss/plantaocultura.xml') - ,(u'Esportes', u'http://oglobo.globo.com/rss/plantaoesportes.xml') - ,(u'Mundo', u'http://oglobo.globo.com/rss/plantaomundo.xml') - ,(u'Pais', u'http://oglobo.globo.com/rss/plantaopais.xml') - ,(u'Rio', u'http://oglobo.globo.com/rss/plantaorio.xml') - ,(u'Saude', u'http://oglobo.globo.com/rss/plantaosaude.xml') - ,(u'Viver Melhor', u'http://oglobo.globo.com/rss/plantaovivermelhor.xml') - ,(u'Economia', u'http://oglobo.globo.com/rss/plantaoeconomia.xml') - ,(u'Tecnologia', u'http://oglobo.globo.com/rss/plantaotecnologia.xml') + (u'Todos os canais', u'http://oglobo.globo.com/rss.xml?completo=true') + ,(u'Ciencia', u'http://oglobo.globo.com/rss.xml?secao=ciencia&completo=true') + ,(u'Educacao', u'http://oglobo.globo.com/rss.xml?secao=educacao&completo=true') + ,(u'Opiniao', u'http://oglobo.globo.com/rss.xml?secao=opiniao&completo=true') + ,(u'Cultura', u'http://oglobo.globo.com/rss.xml?secao=cultura&completo=true') + ,(u'Esportes', u'http://oglobo.globo.com/rss.xml?secao=esportes&completo=true') + ,(u'Mundo', u'http://oglobo.globo.com/rss.xml?secao=mundo&completo=true') + ,(u'Pais', u'http://oglobo.globo.com/rss.xml?secao=pais&completo=true') + ,(u'Rio', u'http://oglobo.globo.com/rss.xml?secao=rio&completo=true') + ,(u'Saude', u'http://oglobo.globo.com/rss.xml?secao=saude&completo=true') + ,(u'Economia', u'http://oglobo.globo.com/rss.xml?secao=economia&completo=true') + ,(u'Tecnologia', u'http://oglobo.globo.com/rss.xml?secao=tecnologia&completo=true') ] + def print_version(self, url): + return url + '?service=print' + def preprocess_html(self, soup): for item in soup.findAll(style=True): del item['style'] return soup - language = 'pt' - + language = 'pt_BR' diff --git a/recipes/smilezilla.recipe b/recipes/smilezilla.recipe new file mode 100644 index 0000000000..242ee8c42a --- /dev/null +++ b/recipes/smilezilla.recipe @@ -0,0 +1,114 @@ + +from calibre.web.feeds.news import BasicNewsRecipe +from calibre.ebooks.BeautifulSoup import BeautifulSoup +from calibre.ptempfile import PersistentTemporaryFile + +class SmileZilla(BasicNewsRecipe): + + title = 'SmileZilla' + language = 'en' + __author__ = "Will" + JOKES_INDEX = 'http://www.smilezilla.com/joke.do' + STORIES_INDEX = 'http://www.smilezilla.com/story.do' + description = 'Daily Jokes and funny stoires' + oldest_article = 1 + remove_tags = [ + ] + keep_only_tags = [] + no_stylesheets = True + simultaneous_downloads = 1 + articles_are_obfuscated = True + encoding = 'utf-8' + + remove_tags = [dict(name='table')] + + counter = {JOKES_INDEX: 0, STORIES_INDEX: 0 } + cache = {} + + def cached_fetch(self, url): + cache = self.cache + + if url in cache: + f = open(cache[url]) + html = f.read() + f.close() + return BeautifulSoup(html, fromEncoding=self.encoding) + + br = BasicNewsRecipe.get_browser() + response = br.open(url) + html = response.read() + soup = BeautifulSoup(html, fromEncoding=self.encoding) + for img in soup.findAll('img',src=True): + if img['src'].startswith('/'): + img['src'] = 'http://www.smilezilla.com' + img['src'] + pt = PersistentTemporaryFile('.html') + pt.write(str(soup.html).encode(self.encoding)) + pt.close() + cache[url] = pt.name + return soup + + def _get_entry(self,soup): + return soup.find('form', attrs={'name':'contentForm'}) + + def _get_section_title(self, soup): + title_div = soup.find('div', attrs={'class':'title'}) + return self.tag_to_string(title_div).strip() + + def parse_index(self): + articles = [] + + soup = self.cached_fetch(self.JOKES_INDEX) + jokes_entry = self._get_entry(soup) + section_title = self._get_section_title(soup) + todays_jokes = [] + for hr in enumerate(jokes_entry.findAll('hr')): + title = 'Joke ' + str(hr[0] + 1) + url = self.JOKES_INDEX + todays_jokes.append({'title':title, 'url':url, + 'description':'', 'date':''}) + articles.append((section_title,todays_jokes)) + + soup = self.cached_fetch(self.STORIES_INDEX) + entry = self._get_entry(soup) + section_title = self._get_section_title(soup) + + todays_stories = [] + for hr in enumerate(entry.findAll('hr')): + title = 'Story ' + str(hr[0] + 1) + current = hr[1] + while True: + current = current.findPrevious() + if current is None: + break + elif current.name == 'hr': + break + elif current.name == 'b': + title = title + ': ' + self.tag_to_string(current) + break + url = self.STORIES_INDEX + todays_stories.append({'title':title, 'url':url, + 'description':'', 'date':''}) + articles.append((section_title,todays_stories)) + + + return articles + + def get_obfuscated_article(self, url): + return self.cache[url] + + + def preprocess_raw_html(self,raw_html, url): + url = self.JOKES_INDEX if (self.cache[self.JOKES_INDEX] in url) else self.STORIES_INDEX + count = self.counter[url] +1 + self.counter[url] = count + soup = self.index_to_soup(raw_html) + entry = self._get_entry(soup) + soup2 = BeautifulSoup('') + body = soup2.find('body') + entries = str(entry).split('
') + body.insert(0,entries[count -1]) + + return str(soup2) + + + diff --git a/resources/images/cover_texture.png b/resources/images/cover_texture.png new file mode 100644 index 0000000000..3fbc804ea8 Binary files /dev/null and b/resources/images/cover_texture.png differ diff --git a/resources/viewer/referencing.js b/resources/viewer/referencing.js index eceea9f31d..7c2a9d0a3f 100644 --- a/resources/viewer/referencing.js +++ b/resources/viewer/referencing.js @@ -60,8 +60,13 @@ function goto_reference(ref) { if (num < 0) {alert("Invalid reference: "+ref); return;} var p = $("p"); if (num >= p.length) {alert("Reference not found: "+ref); return;} - $.scrollTo($(p[num]), 1000, - {onAfter:function(){window.py_bridge.animated_scroll_done()}}); + var dest = $(p[num]); + if (window.paged_display.in_paged_mode) { + var xpos = dest.offset().left; + window.paged_display.scroll_to_xpos(xpos, true, true, 1000); + } else + $.scrollTo(dest, 1000, + {onAfter:function(){window.py_bridge.animated_scroll_done()}}); } diff --git a/session.vim b/session.vim index fbb573e27e..ae2c55bf06 100644 --- a/session.vim +++ b/session.vim @@ -8,6 +8,7 @@ let g:syntastic_cpp_include_dirs = [ \'/usr/include/qt4/QtGui', \'/usr/include/qt4', \'src/qtcurve/common', 'src/qtcurve', + \'/usr/include/ImageMagick', \] let g:syntastic_c_include_dirs = g:syntastic_cpp_include_dirs diff --git a/setup/installer/windows/notes.rst b/setup/installer/windows/notes.rst index 7fe978d30b..0a9c904ff7 100644 --- a/setup/installer/windows/notes.rst +++ b/setup/installer/windows/notes.rst @@ -115,7 +115,7 @@ PyQt4 Compiling instructions:: - python configure.py -c -j5 -e QtCore -e QtGui -e QtSvg -e QtNetwork -e QtWebKit -e QtXmlPatterns --verbose + python configure.py -c -j5 -e QtCore -e QtGui -e QtSvg -e QtNetwork -e QtWebKit -e QtXmlPatterns --verbose --confirm-license nmake nmake install diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py index c53cbd3812..ad599a9d42 100644 --- a/src/calibre/devices/android/driver.py +++ b/src/calibre/devices/android/driver.py @@ -90,6 +90,7 @@ class ANDROID(USBMS): 0x4e22 : [0x0100, 0x226, 0x227, 0x231], 0xb058 : [0x0222, 0x226, 0x227], 0x0ff9 : [0x0226], + 0xc91 : HTC_BCDS, 0xdddd : [0x216], }, @@ -165,7 +166,10 @@ class ANDROID(USBMS): 0x2237: { 0x2208 : [0x0226] }, # Lenovo - 0x17ef : { 0x7421 : [0x0216] }, + 0x17ef : { + 0x7421 : [0x0216], + 0x741b : [0x9999], + }, # Pantech 0x10a9 : { 0x6050 : [0x227] }, @@ -203,7 +207,8 @@ class ANDROID(USBMS): 'GT-I9003_CARD', 'XT912', 'FILE-CD_GADGET', 'RK29_SDK', 'MB855', 'XT910', 'BOOK_A10', 'USB_2.0_DRIVER', 'I9100T', 'P999DW', 'KTABLET_PC', 'INGENIC', 'GT-I9001_CARD', 'USB_2.0_DRIVER', - 'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX'] + 'GT-S5830L_CARD', 'UNIVERSE', 'XT875', 'PRO', '.KOBO_VOX', + 'THINKPAD_TABLET'] 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', diff --git a/src/calibre/devices/apple/driver.py b/src/calibre/devices/apple/driver.py index efc07ed764..929c7b9ba0 100644 --- a/src/calibre/devices/apple/driver.py +++ b/src/calibre/devices/apple/driver.py @@ -2393,8 +2393,8 @@ class ITUNES(DriverBase): foo = self.iTunes.name() except: # Try static binding - import iTunes_glue - self.iTunes = appscript.app('iTunes',terms=iTunes_glue) + import itunes + self.iTunes = appscript.app('iTunes',terms=itunes) try: foo = self.iTunes.name() as_binding = "static" diff --git a/src/calibre/devices/apple/itunes.py b/src/calibre/devices/apple/itunes.py new file mode 100644 index 0000000000..e80bc4c81f --- /dev/null +++ b/src/calibre/devices/apple/itunes.py @@ -0,0 +1,280 @@ +version = 1.1 +path = '/Applications/iTunes.app' + +classes = \ +[('print_settings', 'pset'), + ('application', 'capp'), + ('artwork', 'cArt'), + ('audio_CD_playlist', 'cCDP'), + ('audio_CD_track', 'cCDT'), + ('browser_window', 'cBrW'), + ('device_playlist', 'cDvP'), + ('device_track', 'cDvT'), + ('encoder', 'cEnc'), + ('EQ_preset', 'cEQP'), + ('EQ_window', 'cEQW'), + ('file_track', 'cFlT'), + ('folder_playlist', 'cFoP'), + ('item', 'cobj'), + ('library_playlist', 'cLiP'), + ('playlist', 'cPly'), + ('playlist_window', 'cPlW'), + ('radio_tuner_playlist', 'cRTP'), + ('shared_track', 'cShT'), + ('source', 'cSrc'), + ('track', 'cTrk'), + ('URL_track', 'cURT'), + ('user_playlist', 'cUsP'), + ('visual', 'cVis'), + ('window', 'cwin')] + +enums = \ +[('track_listing', 'kTrk'), + ('album_listing', 'kAlb'), + ('cd_insert', 'kCDi'), + ('standard', 'lwst'), + ('detailed', 'lwdt'), + ('stopped', 'kPSS'), + ('playing', 'kPSP'), + ('paused', 'kPSp'), + ('fast_forwarding', 'kPSF'), + ('rewinding', 'kPSR'), + ('off', 'kRpO'), + ('one', 'kRp1'), + ('all', 'kAll'), + ('small', 'kVSS'), + ('medium', 'kVSM'), + ('large', 'kVSL'), + ('library', 'kLib'), + ('iPod', 'kPod'), + ('audio_CD', 'kACD'), + ('MP3_CD', 'kMCD'), + ('device', 'kDev'), + ('radio_tuner', 'kTun'), + ('shared_library', 'kShd'), + ('unknown', 'kUnk'), + ('albums', 'kSrL'), + ('artists', 'kSrR'), + ('composers', 'kSrC'), + ('displayed', 'kSrV'), + ('songs', 'kSrS'), + ('none', 'kNon'), + ('Books', 'kSpA'), + ('folder', 'kSpF'), + ('Genius', 'kSpG'), + ('iTunes_U', 'kSpU'), + ('Library', 'kSpL'), + ('Movies', 'kSpI'), + ('Music', 'kSpZ'), + ('Party_Shuffle', 'kSpS'), + ('Podcasts', 'kSpP'), + ('Purchased_Music', 'kSpM'), + ('TV_Shows', 'kSpT'), + ('movie', 'kVdM'), + ('music_video', 'kVdV'), + ('TV_show', 'kVdT'), + ('user', 'kRtU'), + ('computed', 'kRtC')] + +properties = \ +[('copies', 'lwcp'), + ('collating', 'lwcl'), + ('starting_page', 'lwfp'), + ('ending_page', 'lwlp'), + ('pages_across', 'lwla'), + ('pages_down', 'lwld'), + ('error_handling', 'lweh'), + ('requested_print_time', 'lwqt'), + ('printer_features', 'lwpf'), + ('fax_number', 'faxn'), + ('target_printer', 'trpr'), + ('current_encoder', 'pEnc'), + ('current_EQ_preset', 'pEQP'), + ('current_playlist', 'pPla'), + ('current_stream_title', 'pStT'), + ('current_stream_URL', 'pStU'), + ('current_track', 'pTrk'), + ('current_visual', 'pVis'), + ('EQ_enabled', 'pEQ '), + ('fixed_indexing', 'pFix'), + ('frontmost', 'pisf'), + ('full_screen', 'pFSc'), + ('name', 'pnam'), + ('mute', 'pMut'), + ('player_position', 'pPos'), + ('player_state', 'pPlS'), + ('selection', 'sele'), + ('sound_volume', 'pVol'), + ('version', 'vers'), + ('visuals_enabled', 'pVsE'), + ('visual_size', 'pVSz'), + ('data', 'pPCT'), + ('description', 'pDes'), + ('downloaded', 'pDlA'), + ('format', 'pFmt'), + ('kind', 'pKnd'), + ('raw_data', 'pRaw'), + ('artist', 'pArt'), + ('compilation', 'pAnt'), + ('composer', 'pCmp'), + ('disc_count', 'pDsC'), + ('disc_number', 'pDsN'), + ('genre', 'pGen'), + ('year', 'pYr '), + ('location', 'pLoc'), + ('minimized', 'pMin'), + ('view', 'pPly'), + ('band_1', 'pEQ1'), + ('band_2', 'pEQ2'), + ('band_3', 'pEQ3'), + ('band_4', 'pEQ4'), + ('band_5', 'pEQ5'), + ('band_6', 'pEQ6'), + ('band_7', 'pEQ7'), + ('band_8', 'pEQ8'), + ('band_9', 'pEQ9'), + ('band_10', 'pEQ0'), + ('modifiable', 'pMod'), + ('preamp', 'pEQA'), + ('update_tracks', 'pUTC'), + ('container', 'ctnr'), + ('id', 'ID '), + ('index', 'pidx'), + ('persistent_ID', 'pPIS'), + ('duration', 'pDur'), + ('parent', 'pPlP'), + ('shuffle', 'pShf'), + ('size', 'pSiz'), + ('song_repeat', 'pRpt'), + ('special_kind', 'pSpK'), + ('time', 'pTim'), + ('visible', 'pvis'), + ('capacity', 'capa'), + ('free_space', 'frsp'), + ('album', 'pAlb'), + ('album_artist', 'pAlA'), + ('album_rating', 'pAlR'), + ('album_rating_kind', 'pARk'), + ('bit_rate', 'pBRt'), + ('bookmark', 'pBkt'), + ('bookmarkable', 'pBkm'), + ('bpm', 'pBPM'), + ('category', 'pCat'), + ('comment', 'pCmt'), + ('database_ID', 'pDID'), + ('date_added', 'pAdd'), + ('enabled', 'enbl'), + ('episode_ID', 'pEpD'), + ('episode_number', 'pEpN'), + ('EQ', 'pEQp'), + ('finish', 'pStp'), + ('gapless', 'pGpl'), + ('grouping', 'pGrp'), + ('long_description', 'pLds'), + ('lyrics', 'pLyr'), + ('modification_date', 'asmo'), + ('played_count', 'pPlC'), + ('played_date', 'pPlD'), + ('podcast', 'pTPc'), + ('rating', 'pRte'), + ('rating_kind', 'pRtk'), + ('release_date', 'pRlD'), + ('sample_rate', 'pSRt'), + ('season_number', 'pSeN'), + ('shufflable', 'pSfa'), + ('skipped_count', 'pSkC'), + ('skipped_date', 'pSkD'), + ('show', 'pShw'), + ('sort_album', 'pSAl'), + ('sort_artist', 'pSAr'), + ('sort_album_artist', 'pSAA'), + ('sort_name', 'pSNm'), + ('sort_composer', 'pSCm'), + ('sort_show', 'pSSN'), + ('start', 'pStr'), + ('track_count', 'pTrC'), + ('track_number', 'pTrN'), + ('unplayed', 'pUnp'), + ('video_kind', 'pVdK'), + ('volume_adjustment', 'pAdj'), + ('address', 'pURL'), + ('shared', 'pShr'), + ('smart', 'pSmt'), + ('bounds', 'pbnd'), + ('closeable', 'hclb'), + ('collapseable', 'pWSh'), + ('collapsed', 'wshd'), + ('position', 'ppos'), + ('resizable', 'prsz'), + ('zoomable', 'iszm'), + ('zoomed', 'pzum')] + +elements = \ +[('artworks', 'cArt'), + ('audio_CD_playlists', 'cCDP'), + ('audio_CD_tracks', 'cCDT'), + ('browser_windows', 'cBrW'), + ('device_playlists', 'cDvP'), + ('device_tracks', 'cDvT'), + ('encoders', 'cEnc'), + ('EQ_presets', 'cEQP'), + ('EQ_windows', 'cEQW'), + ('file_tracks', 'cFlT'), + ('folder_playlists', 'cFoP'), + ('items', 'cobj'), + ('library_playlists', 'cLiP'), + ('playlists', 'cPly'), + ('playlist_windows', 'cPlW'), + ('radio_tuner_playlists', 'cRTP'), + ('shared_tracks', 'cShT'), + ('sources', 'cSrc'), + ('tracks', 'cTrk'), + ('URL_tracks', 'cURT'), + ('user_playlists', 'cUsP'), + ('visuals', 'cVis'), + ('windows', 'cwin'), + ('application', 'capp'), + ('print_settings', 'pset')] + +commands = \ +[('set', 'coresetd', [('to', 'data')]), + ('exists', 'coredoex', []), + ('move', 'coremove', [('to', 'insh')]), + ('subscribe', 'hookpSub', []), + ('playpause', 'hookPlPs', []), + ('download', 'hookDwnl', []), + ('close', 'coreclos', []), + ('open', 'aevtodoc', []), + ('open_location', 'GURLGURL', []), + ('quit', 'aevtquit', []), + ('pause', 'hookPaus', []), + ('make', + 'corecrel', + [('new', 'kocl'), ('at', 'insh'), ('with_properties', 'prdt')]), + ('duplicate', 'coreclon', [('to', 'insh')]), + ('print_', + 'aevtpdoc', + [('print_dialog', 'pdlg'), + ('with_properties', 'prdt'), + ('kind', 'pKnd'), + ('theme', 'pThm')]), + ('add', 'hookAdd ', [('to', 'insh')]), + ('rewind', 'hookRwnd', []), + ('play', 'hookPlay', [('once', 'POne')]), + ('run', 'aevtoapp', []), + ('resume', 'hookResu', []), + ('updatePodcast', 'hookUpd1', []), + ('next_track', 'hookNext', []), + ('stop', 'hookStop', []), + ('search', 'hookSrch', [('for_', 'pTrm'), ('only', 'pAre')]), + ('updateAllPodcasts', 'hookUpdp', []), + ('update', 'hookUpdt', []), + ('previous_track', 'hookPrev', []), + ('fast_forward', 'hookFast', []), + ('count', 'corecnte', [('each', 'kocl')]), + ('reveal', 'hookRevl', []), + ('convert', 'hookConv', []), + ('eject', 'hookEjct', []), + ('back_track', 'hookBack', []), + ('refresh', 'hookRfrs', []), + ('delete', 'coredelo', [])] diff --git a/src/calibre/devices/udisks.py b/src/calibre/devices/udisks.py index 54a21cacff..18771dbeb2 100644 --- a/src/calibre/devices/udisks.py +++ b/src/calibre/devices/udisks.py @@ -5,10 +5,6 @@ __license__ = 'GPL v3' __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' -# First repeat after me: Linux desktop infrastructure is designed by a -# committee of rabid monkeys on crack. They would not know a decent desktop if -# it was driving the rabid monkey extermination truck that runs them over. - import os, dbus, re def node_mountpoint(node): @@ -23,13 +19,20 @@ def node_mountpoint(node): return de_mangle(line[1]) return None +class NoUDisks1(Exception): + pass class UDisks(object): def __init__(self): self.bus = dbus.SystemBus() - self.main = dbus.Interface(self.bus.get_object('org.freedesktop.UDisks', + try: + self.main = dbus.Interface(self.bus.get_object('org.freedesktop.UDisks', '/org/freedesktop/UDisks'), 'org.freedesktop.UDisks') + except dbus.exceptions.DBusException as e: + if getattr(e, '_dbus_error_name', None) == 'org.freedesktop.DBus.Error.ServiceUnknown': + raise NoUDisks1() + raise def device(self, device_node_path): devpath = self.main.FindDeviceByDeviceFile(device_node_path) @@ -67,6 +70,7 @@ class UDisks2(object): BLOCK = 'org.freedesktop.UDisks2.Block' FILESYSTEM = 'org.freedesktop.UDisks2.Filesystem' + DRIVE = 'org.freedesktop.UDisks2.Drive' def __init__(self): self.bus = dbus.SystemBus() @@ -131,6 +135,21 @@ class UDisks2(object): raise return mp + def unmount(self, device_node_path): + d = self.device(device_node_path) + d.Unmount({'force':True, 'auth.no_user_interaction':True}, + dbus_interface=self.FILESYSTEM) + + def drive_for_device(self, device): + drive = device.Get(self.BLOCK, 'Drive', + dbus_interface='org.freedesktop.DBus.Properties') + return self.bus.get_object('org.freedesktop.UDisks2', drive) + + def eject(self, device_node_path): + drive = self.drive_for_device(self.device(device_node_path)) + drive.Eject({'auth.no_user_interaction':True}, + dbus_interface=self.DRIVE) + def get_udisks(ver=None): if ver is None: try: @@ -140,7 +159,6 @@ def get_udisks(ver=None): return u return UDisks2() if ver == 2 else UDisks() - def mount(node_path): u = UDisks() u.mount(node_path) diff --git a/src/calibre/ebooks/__init__.py b/src/calibre/ebooks/__init__.py index 09cc2fbaaf..7a61c7cd96 100644 --- a/src/calibre/ebooks/__init__.py +++ b/src/calibre/ebooks/__init__.py @@ -187,7 +187,9 @@ def calibre_cover(title, author_string, series_string=None, lines.append(TextLine(series_string, author_size)) if logo_path is None: logo_path = I('library.png') - return create_cover_page(lines, logo_path, output_format='jpg') + return create_cover_page(lines, logo_path, output_format='jpg', + texture_opacity=0.3, texture_data=I('cover_texture.png', + data=True)) UNIT_RE = re.compile(r'^(-*[0-9]*[.]?[0-9]*)\s*(%|em|ex|en|px|mm|cm|in|pt|pc)$') diff --git a/src/calibre/ebooks/metadata/sources/amazon.py b/src/calibre/ebooks/metadata/sources/amazon.py index 6764e2f6f7..219a0e02ca 100644 --- a/src/calibre/ebooks/metadata/sources/amazon.py +++ b/src/calibre/ebooks/metadata/sources/amazon.py @@ -687,7 +687,11 @@ class Amazon(Source): return True for div in root.xpath(r'//div[starts-with(@id, "result_")]'): - for a in div.xpath(r'descendant::a[@class="title" and @href]'): + links = div.xpath(r'descendant::a[@class="title" and @href]') + if not links: + # New amazon markup + links = div.xpath('descendant::h3/a[@href]') + for a in links: title = tostring(a, method='text', encoding=unicode) if title_ok(title): matches.append(a.get('href')) diff --git a/src/calibre/ebooks/metadata/sources/isbndb.py b/src/calibre/ebooks/metadata/sources/isbndb.py index 7e15ad275e..567673b704 100644 --- a/src/calibre/ebooks/metadata/sources/isbndb.py +++ b/src/calibre/ebooks/metadata/sources/isbndb.py @@ -154,10 +154,11 @@ class ISBNDB(Source): total_results = int(bl.get('total_results')) shown_results = int(bl.get('shown_results')) for bd in bl.xpath('.//BookData'): - isbn = check_isbn(bd.get('isbn13', bd.get('isbn', None))) - if not isbn: + isbn = check_isbn(bd.get('isbn', None)) + isbn13 = check_isbn(bd.get('isbn13', None)) + if not isbn and not isbn13: continue - if orig_isbn and isbn != orig_isbn: + if orig_isbn and orig_isbn not in {isbn, isbn13}: continue title = tostring(bd.find('Title')) if not title: @@ -173,10 +174,6 @@ class ISBNDB(Source): if not authors: continue comments = tostring(bd.find('Summary')) - if not comments: - # Require comments, since without them the result is useless - # anyway - continue id_ = (title, tuple(authors)) if id_ in seen: continue diff --git a/src/calibre/ebooks/metadata/sources/test.py b/src/calibre/ebooks/metadata/sources/test.py index 4853035b27..9fa70aba07 100644 --- a/src/calibre/ebooks/metadata/sources/test.py +++ b/src/calibre/ebooks/metadata/sources/test.py @@ -167,7 +167,8 @@ def test_identify(tests): # {{{ # }}} -def test_identify_plugin(name, tests, modify_plugin=lambda plugin:None): # {{{ +def test_identify_plugin(name, tests, modify_plugin=lambda plugin:None, + fail_missing_meta=True): # {{{ ''' :param name: Plugin name :param tests: List of 2-tuples. Each two tuple is of the form (args, @@ -246,7 +247,8 @@ def test_identify_plugin(name, tests, modify_plugin=lambda plugin:None): # {{{ None] if not good: prints('Failed to find', plugin.test_fields(possibles[0])) - raise SystemExit(1) + if fail_missing_meta: + raise SystemExit(1) if results[0] is not possibles[0]: prints('Most relevant result failed the tests') @@ -263,21 +265,22 @@ def test_identify_plugin(name, tests, modify_plugin=lambda plugin:None): # {{{ results.append(rq.get_nowait()) except Empty: break - if not results: + if not results and fail_missing_meta: prints('Cover download failed') raise SystemExit(1) - cdata = results[0] - cover = os.path.join(tdir, plugin.name.replace(' ', - '')+'-%s-cover.jpg'%sanitize_file_name2(mi.title.replace(' ', - '_'))) - with open(cover, 'wb') as f: - f.write(cdata[-1]) + elif results: + cdata = results[0] + cover = os.path.join(tdir, plugin.name.replace(' ', + '')+'-%s-cover.jpg'%sanitize_file_name2(mi.title.replace(' ', + '_'))) + with open(cover, 'wb') as f: + f.write(cdata[-1]) - prints('Cover downloaded to:', cover) + prints('Cover downloaded to:', cover) - if len(cdata[-1]) < 10240: - prints('Downloaded cover too small') - raise SystemExit(1) + if len(cdata[-1]) < 10240: + prints('Downloaded cover too small') + raise SystemExit(1) prints('Average time per query', sum(times)/len(times)) diff --git a/src/calibre/ebooks/mobi/writer8/skeleton.py b/src/calibre/ebooks/mobi/writer8/skeleton.py index ae083163d2..5db6ee0b5c 100644 --- a/src/calibre/ebooks/mobi/writer8/skeleton.py +++ b/src/calibre/ebooks/mobi/writer8/skeleton.py @@ -111,7 +111,7 @@ class Skeleton(object): self.chunks = chunks self.skeleton = self.render(root) - self.body_offset = self.skeleton.find('')[0]) + len(text) + 1 end_length = len(raw.rpartition(b'<')[-1]) + 1 self.metrics[tag.get('aid')] = Metric(start_length, end_length) diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py index 7e76d4cc0d..a852cff031 100644 --- a/src/calibre/ebooks/oeb/base.py +++ b/src/calibre/ebooks/oeb/base.py @@ -469,6 +469,8 @@ class DirContainer(object): return f.write(data) def exists(self, path): + if not path: + return False try: path = os.path.join(self.rootdir, self._unquote(path)) except ValueError: #Happens if path contains quoted special chars diff --git a/src/calibre/ebooks/oeb/display/indexing.coffee b/src/calibre/ebooks/oeb/display/indexing.coffee index 11f73c1504..48f0697506 100644 --- a/src/calibre/ebooks/oeb/display/indexing.coffee +++ b/src/calibre/ebooks/oeb/display/indexing.coffee @@ -6,20 +6,34 @@ Released under the GPLv3 License ### -body_height = () -> - db = document.body - dde = document.documentElement - if db? and dde? - return Math.max(db.scrollHeight, dde.scrollHeight, db.offsetHeight, - dde.offsetHeight, db.clientHeight, dde.clientHeight) - return 0 +window_scroll_pos = (win=window) -> # {{{ + if typeof(win.pageXOffset) == 'number' + x = win.pageXOffset + y = win.pageYOffset + else # IE < 9 + if document.body and ( document.body.scrollLeft or document.body.scrollTop ) + x = document.body.scrollLeft + y = document.body.scrollTop + else if document.documentElement and ( document.documentElement.scrollLeft or document.documentElement.scrollTop) + y = document.documentElement.scrollTop + x = document.documentElement.scrollLeft + return [x, y] +# }}} -abstop = (elem) -> - ans = elem.offsetTop - while elem.offsetParent - elem = elem.offsetParent - ans += elem.offsetTop - return ans +viewport_to_document = (x, y, doc=window?.document) -> # {{{ + until doc == window.document + # We are in a frame + frame = doc.defaultView.frameElement + rect = frame.getBoundingClientRect() + x += rect.left + y += rect.top + doc = frame.ownerDocument + win = doc.defaultView + [wx, wy] = window_scroll_pos(win) + x += wx + y += wy + return [x, y] +# }}} class BookIndexing ### @@ -33,7 +47,7 @@ class BookIndexing constructor: () -> this.cache = {} - this.body_height_at_last_check = null + this.last_check = [null, null] cache_valid: (anchors) -> for a in anchors @@ -45,7 +59,9 @@ class BookIndexing return true anchor_positions: (anchors, use_cache=false) -> - if use_cache and body_height() == this.body_height_at_last_check and this.cache_valid(anchors) + body = document.body + doc_constant = body.scrollHeight == this.last_check[1] and body.scrollWidth == this.last_check[0] + if use_cache and doc_constant and this.cache_valid(anchors) return this.cache ans = {} @@ -56,19 +72,24 @@ class BookIndexing try result = document.evaluate( ".//*[local-name() = 'a' and @name='#{ anchor }']", - document.body, null, + body, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null) elem = result.singleNodeValue catch error # The anchor had a ' or other invalid char elem = null if elem == null - pos = body_height() + 10000 + pos = [body.scrollWidth+1000, body.scrollHeight+1000] else - pos = abstop(elem) + br = elem.getBoundingClientRect() + pos = viewport_to_document(br.left, br.top, elem.ownerDocument) + + if window.paged_display?.in_paged_mode + pos[0] = window.paged_display.column_at(pos[0]) ans[anchor] = pos + this.cache = ans - this.body_height_at_last_check = body_height() + this.last_check = [body.scrollWidth, body.scrollHeight] return ans if window? diff --git a/src/calibre/ebooks/oeb/display/paged.coffee b/src/calibre/ebooks/oeb/display/paged.coffee index 142fb5f887..ff750ff3ba 100644 --- a/src/calibre/ebooks/oeb/display/paged.coffee +++ b/src/calibre/ebooks/oeb/display/paged.coffee @@ -50,14 +50,12 @@ absleft = (elem) -> # {{{ # }}} class PagedDisplay - ### - This class is a namespace to expose functions via the - window.paged_display object. The most important functions are: - - set_geometry(): sets the parameters used to layout text in paged mode - - layout(): causes the currently loaded document to be laid out in columns. - ### + # This class is a namespace to expose functions via the + # window.paged_display object. The most important functions are: + # + # set_geometry(): sets the parameters used to layout text in paged mode + # + # layout(): causes the currently loaded document to be laid out in columns. constructor: () -> if not this instanceof arguments.callee @@ -163,7 +161,7 @@ class PagedDisplay xpos = Math.floor(document.body.scrollWidth * frac) this.scroll_to_xpos(xpos) - scroll_to_xpos: (xpos) -> + scroll_to_xpos: (xpos, animated=false, notify=false, duration=1000) -> # Scroll so that the column containing xpos is the left most column in # the viewport if typeof(xpos) != 'number' @@ -172,12 +170,47 @@ class PagedDisplay if this.is_full_screen_layout window.scrollTo(0, 0) return - pos = 0 - until (pos <= xpos < pos + this.page_width) - pos += this.page_width + pos = Math.floor(xpos/this.page_width) * this.page_width limit = document.body.scrollWidth - this.screen_width pos = limit if pos > limit - window.scrollTo(pos, 0) + if animated + this.animated_scroll(pos, duration=1000, notify=notify) + else + window.scrollTo(pos, 0) + + column_at: (xpos) -> + # Return the number of the column that contains xpos + return Math.floor(xpos/this.page_width) + + column_boundaries: () -> + # Return the column numbers at the left edge and after the right edge + # of the viewport + l = this.column_at(window.pageXOffset + 10) + return [l, l + this.cols_per_screen] + + animated_scroll: (pos, duration=1000, notify=true) -> + # Scroll the window to X-position pos in an animated fashion over + # duration milliseconds. If notify is true, py_bridge.animated_scroll_done is + # called. + delta = pos - window.pageXOffset + interval = 50 + steps = Math.floor(duration/interval) + step_size = Math.floor(delta/steps) + this.current_scroll_animation = {target:pos, step_size:step_size, interval:interval, notify:notify, fn: () => + a = this.current_scroll_animation + npos = window.pageXOffset + a.step_size + completed = false + if Math.abs(npos - a.target) < Math.abs(a.step_size) + completed = true + npos = a.target + window.scrollTo(npos, 0) + if completed + if notify + window.py_bridge.animated_scroll_done() + else + setTimeout(a.fn, a.interval) + } + this.current_scroll_animation.fn() current_pos: (frac) -> # The current scroll position as a fraction between 0 and 1 @@ -192,10 +225,7 @@ class PagedDisplay if this.is_full_screen_layout return 0 x = window.pageXOffset + Math.max(10, this.current_margin_side) - edge = Math.floor(x/this.page_width) * this.page_width - while edge < x - edge += this.page_width - return edge - this.page_width + return Math.floor(x/this.page_width) * this.page_width next_screen_location: () -> # The position to scroll to for the next screen (which could contain @@ -329,7 +359,6 @@ if window? window.paged_display = new PagedDisplay() # TODO: -# Go to reference positions -# Indexing # Resizing of images # Full screen mode +# Highlight on jump_to_anchor diff --git a/src/calibre/ebooks/oeb/transforms/filenames.py b/src/calibre/ebooks/oeb/transforms/filenames.py index 00b71ea8be..ddf4def30c 100644 --- a/src/calibre/ebooks/oeb/transforms/filenames.py +++ b/src/calibre/ebooks/oeb/transforms/filenames.py @@ -105,14 +105,14 @@ class UniqueFilenames(object): # {{{ base, ext = posixpath.splitext(item.href) nhref = base + suffix + ext nhref = oeb.manifest.generate(href=nhref)[1] + spine_pos = item.spine_position + oeb.manifest.remove(item) nitem = oeb.manifest.add(item.id, nhref, item.media_type, data=data, fallback=item.fallback) self.seen_filenames.add(posixpath.basename(nhref)) self.rename_map[item.href] = nhref - if item.spine_position is not None: - oeb.spine.insert(item.spine_position, nitem, item.linear) - oeb.spine.remove(item) - oeb.manifest.remove(item) + if spine_pos is not None: + oeb.spine.insert(spine_pos, nitem, item.linear) else: self.seen_filenames.add(fname) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 3ae1633230..d5e93c48c8 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -248,6 +248,18 @@ def available_width(): desktop = QCoreApplication.instance().desktop() return desktop.availableGeometry().width() +def get_windows_color_depth(): + import win32gui, win32con, win32print + hwin = win32gui.GetDesktopWindow() + hwindc = win32gui.GetWindowDC(hwin) + ans = win32print.GetDeviceCaps(hwindc, win32con.BITSPIXEL) + win32gui.ReleaseDC(hwin, hwindc) + return ans + +def get_screen_dpi(): + d = QApplication.desktop() + return (d.logicalDpiX(), d.logicalDpiY()) + _is_widescreen = None def is_widescreen(): @@ -791,7 +803,18 @@ class Application(QApplication): font.setStretch(s) QApplication.setFont(font) - if force_calibre_style or gprefs['ui_style'] != 'system': + depth_ok = True + if iswindows: + # There are some people that still run 16 bit winxp installs. The + # new style does not render well on 16bit machines. + try: + depth_ok = get_windows_color_depth() >= 32 + except: + import traceback + traceback.print_exc() + + if force_calibre_style or (depth_ok and gprefs['ui_style'] != + 'system'): self.load_calibre_style() else: st = self.style() diff --git a/src/calibre/gui2/dialogs/catalog.ui b/src/calibre/gui2/dialogs/catalog.ui index 7f3951b87e..cf51ac8848 100644 --- a/src/calibre/gui2/dialogs/catalog.ui +++ b/src/calibre/gui2/dialogs/catalog.ui @@ -15,7 +15,7 @@ - :/images/library.png:/images/library.png + :/images/lt.png:/images/lt.png diff --git a/src/calibre/gui2/dialogs/confirm_delete_location.ui b/src/calibre/gui2/dialogs/confirm_delete_location.ui index 212d96584f..f375eb7207 100644 --- a/src/calibre/gui2/dialogs/confirm_delete_location.ui +++ b/src/calibre/gui2/dialogs/confirm_delete_location.ui @@ -54,7 +54,7 @@ - :/images/library.png:/images/library.png + :/images/lt.png:/images/lt.png diff --git a/src/calibre/gui2/dialogs/conversion_error.ui b/src/calibre/gui2/dialogs/conversion_error.ui index c51c868d1b..0b21147fee 100644 --- a/src/calibre/gui2/dialogs/conversion_error.ui +++ b/src/calibre/gui2/dialogs/conversion_error.ui @@ -14,7 +14,7 @@ - :/images/library.png:/images/library.png + :/images/lt.png:/images/lt.png diff --git a/src/calibre/gui2/dialogs/tag_list_editor.py b/src/calibre/gui2/dialogs/tag_list_editor.py index aae68c4966..298641a9df 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.py +++ b/src/calibre/gui2/dialogs/tag_list_editor.py @@ -5,7 +5,7 @@ from PyQt4.Qt import (Qt, QDialog, QTableWidgetItem, QIcon, QByteArray, QString, QSize) from calibre.gui2.dialogs.tag_list_editor_ui import Ui_TagListEditor -from calibre.gui2 import question_dialog, error_dialog, gprefs +from calibre.gui2 import question_dialog, error_dialog, info_dialog, gprefs from calibre.utils.icu import sort_key class NameTableWidgetItem(QTableWidgetItem): @@ -149,6 +149,9 @@ class TagListEditor(QDialog, Ui_TagListEditor): self.table.itemChanged.connect(self.finish_editing) self.buttonBox.accepted.connect(self.accepted) + self.search_box.initialize('tag_list_search_box_' + cat_name) + self.search_button.clicked.connect(self.search_clicked) + try: geom = gprefs.get('tag_list_editor_dialog_geometry', None) if geom is not None: @@ -158,6 +161,26 @@ class TagListEditor(QDialog, Ui_TagListEditor): except: pass + def search_clicked(self): + search_for = icu_lower(unicode(self.search_box.text())) + if not search_for: + error_dialog(self, _('Find'), _('You must enter some text to search for'), + show=True, show_copy_button=False) + return + row = self.table.currentRow() + if row < 0: + row = 0 + rows = self.table.rowCount() + for i in range(0, rows): + row += 1 + if row >= rows: + row = 0 + item = self.table.item(row, 0) + if search_for in icu_lower(unicode(item.text())): + self.table.setCurrentItem(item) + return + info_dialog(self, _('Find'), _('No tag found'), show=True, show_copy_button=False) + def table_column_resized(self, col, old, new): self.table_column_widths = [] for c in range(0, self.table.columnCount()): diff --git a/src/calibre/gui2/dialogs/tag_list_editor.ui b/src/calibre/gui2/dialogs/tag_list_editor.ui index ccc404bf9c..cd36d57044 100644 --- a/src/calibre/gui2/dialogs/tag_list_editor.ui +++ b/src/calibre/gui2/dialogs/tag_list_editor.ui @@ -18,75 +18,88 @@ :/images/chapters.png:/images/chapters.png - - + + - - - - - - - Delete item from database. This will unapply the item from all books and then remove it from the database. - - - ... - - - - :/images/trash.png:/images/trash.png - - - - 32 - 32 - - - - - - - - Rename the item in every book where it is used. - - - ... - - - - :/images/edit_input.png:/images/edit_input.png - - - - 32 - 32 - - - - Ctrl+S - - - - - - - - - true - - - QAbstractItemView::ExtendedSelection - - - QAbstractItemView::SelectRows - - - - + + + Search for an item in the Tag column + + + + + + + Find + + + Copy the selected color name to the clipboard + + - + + + + + + Delete item from database. This will unapply the item from all books and then remove it from the database. + + + ... + + + + :/images/trash.png:/images/trash.png + + + + 32 + 32 + + + + + + + + Rename the item in every book where it is used. + + + ... + + + + :/images/edit_input.png:/images/edit_input.png + + + + 32 + 32 + + + + Ctrl+S + + + + + + + + + true + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + + Qt::Horizontal @@ -101,6 +114,13 @@ + + + HistoryLineEdit + QLineEdit +
widgets.h
+
+
diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py index eadf97191a..df277aa13c 100644 --- a/src/calibre/gui2/layout.py +++ b/src/calibre/gui2/layout.py @@ -255,7 +255,7 @@ class MainWindowMixin(object): # {{{ def __init__(self, db): self.setObjectName('MainWindow') - self.setWindowIcon(QIcon(I('library.png'))) + self.setWindowIcon(QIcon(I('lt.png'))) self.setWindowTitle(__appname__) self.setContextMenuPolicy(Qt.NoContextMenu) diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 94c2ffa384..9b80b7bddc 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -60,7 +60,7 @@ def init_qt(args): QCoreApplication.setApplicationName(APP_UID) app = Application(args) actions = tuple(Main.create_application_menubar()) - app.setWindowIcon(QIcon(I('library.png'))) + app.setWindowIcon(QIcon(I('lt.png'))) return app, opts, args, actions @@ -323,6 +323,10 @@ def communicate(opts, args): if opts.shutdown_running_calibre: t.conn.send('shutdown:') + from calibre.utils.lock import singleinstance + prints(_('Shutdown command sent, waiting for shutdown...')) + while not singleinstance('calibre GUI'): + time.sleep(0.1) else: if len(args) > 1: args[1] = os.path.abspath(args[1]) diff --git a/src/calibre/gui2/ui.py b/src/calibre/gui2/ui.py index 713aa77e29..a597445f43 100644 --- a/src/calibre/gui2/ui.py +++ b/src/calibre/gui2/ui.py @@ -228,7 +228,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{ self.default_thumbnail = None self.tb_wrapper = textwrap.TextWrapper(width=40) self.viewers = collections.deque() - self.system_tray_icon = SystemTrayIcon(QIcon(I('library.png')), self) + self.system_tray_icon = SystemTrayIcon(QIcon(I('lt.png')), self) self.system_tray_icon.setToolTip('calibre') self.system_tray_icon.tooltip_requested.connect( self.job_manager.show_tooltip) diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 8c6da27d71..a711442ba2 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -202,19 +202,31 @@ class Document(QWebPage): # {{{ if not isinstance(self.anchor_positions, dict): # Some weird javascript error happened self.anchor_positions = {} - return self.anchor_positions + return {k:tuple(v) for k, v in self.anchor_positions.iteritems()} def switch_to_paged_mode(self, onresize=False): + if onresize and not self.loaded_javascript: + return side_margin = self.javascript('window.paged_display.layout()', typ=int) # Setup the contents size to ensure that there is a right most margin. # Without this webkit renders the final column with no margin, as the # columns extend beyond the boundaries (and margin) of body mf = self.mainFrame() sz = mf.contentsSize() - if sz.width() > self.window_width: - sz.setWidth(sz.width()+side_margin) + scroll_width = self.javascript('document.body.scrollWidth', int) + # At this point sz.width() is not reliable, presumably because Qt + # has not yet been updated + if scroll_width > self.window_width: + sz.setWidth(scroll_width+side_margin) self.setPreferredContentsSize(sz) + @property + def column_boundaries(self): + if not self.loaded_javascript: + return (0, 1) + self.javascript(u'py_bridge.value = paged_display.column_boundaries()') + return tuple(self.bridge_value) + def after_resize(self): if self.in_paged_mode: self.setPreferredContentsSize(QSize()) @@ -294,6 +306,7 @@ class Document(QWebPage): # {{{ self.mainFrame().setScrollPosition(QPoint(x, y)) def jump_to_anchor(self, anchor): + if not self.loaded_javascript: return self.javascript('window.paged_display.jump_to_anchor("%s")'%anchor) def element_ypos(self, elem): @@ -352,7 +365,7 @@ class Document(QWebPage): # {{{ except ZeroDivisionError: return 0. def fset(self, val): - if self.in_paged_mode: + if self.in_paged_mode and self.loaded_javascript: self.javascript('paged_display.scroll_to_pos(%f)'%val) else: npos = val * (self.height - self.window_height) @@ -555,6 +568,18 @@ class DocumentView(QWebView): # {{{ return (self.document.ypos, self.document.ypos + self.document.window_height) + @property + def viewport_rect(self): + # (left, top, right, bottom) of the viewport in document co-ordinates + # When in paged mode, left and right are the numbers of the columns + # at the left edge and *after* the right edge of the viewport + d = self.document + if d.in_paged_mode: + l, r = d.column_boundaries + else: + l, r = d.xpos, d.xpos + d.window_width + return (l, d.ypos, r, d.ypos + d.window_height) + def link_hovered(self, link, text, context): link, text = unicode(link), unicode(text) if link: diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index c74fd649de..08ab731e51 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -138,7 +138,9 @@ class Reference(QLineEdit): self.editingFinished.connect(self.editing_finished) def editing_finished(self): - self.goto.emit(unicode(self.text())) + text = unicode(self.text()) + self.setText('') + self.goto.emit(text) class RecentAction(QAction): @@ -681,7 +683,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): if hasattr(self, 'current_index'): entry = self.toc_model.next_entry(self.current_index, self.view.document.read_anchor_positions(), - self.view.scroll_pos) + self.view.viewport_rect, self.view.document.in_paged_mode) if entry is not None: self.pending_goto_next_section = ( self.toc_model.currently_viewed_entry, entry, False) @@ -691,7 +693,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer): if hasattr(self, 'current_index'): entry = self.toc_model.next_entry(self.current_index, self.view.document.read_anchor_positions(), - self.view.scroll_pos, backwards=True) + self.view.viewport_rect, self.view.document.in_paged_mode, + backwards=True) if entry is not None: self.pending_goto_next_section = ( self.toc_model.currently_viewed_entry, entry, True) @@ -703,7 +706,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer): if anchor_positions is None: anchor_positions = self.view.document.read_anchor_positions() items = self.toc_model.update_indexing_state(self.current_index, - self.view.scroll_pos, anchor_positions) + self.view.viewport_rect, anchor_positions, + self.view.document.in_paged_mode) if items: self.toc.scrollTo(items[-1].index()) if pgns is not None: @@ -712,7 +716,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer): if pgns[0] is self.toc_model.currently_viewed_entry: entry = self.toc_model.next_entry(self.current_index, self.view.document.read_anchor_positions(), - self.view.scroll_pos, + self.view.viewport_rect, + self.view.document.in_paged_mode, backwards=pgns[2], current_entry=pgns[1]) if entry is not None: self.pending_goto_next_section = ( diff --git a/src/calibre/gui2/viewer/toc.py b/src/calibre/gui2/viewer/toc.py index ae03b3ed26..b0e97bea65 100644 --- a/src/calibre/gui2/viewer/toc.py +++ b/src/calibre/gui2/viewer/toc.py @@ -93,9 +93,19 @@ class TOCItem(QStandardItem): def type(cls): return QStandardItem.UserType+10 - def update_indexing_state(self, spine_index, scroll_pos, anchor_map): + def update_indexing_state(self, spine_index, viewport_rect, anchor_map, + in_paged_mode): + if in_paged_mode: + self.update_indexing_state_paged(spine_index, viewport_rect, + anchor_map) + else: + self.update_indexing_state_unpaged(spine_index, viewport_rect, + anchor_map) + + def update_indexing_state_unpaged(self, spine_index, viewport_rect, + anchor_map): is_being_viewed = False - top, bottom = scroll_pos + top, bottom = viewport_rect[1], viewport_rect[3] # We use bottom-25 in the checks below to account for the case where # the next entry has some invisible margin that just overlaps with the # bottom of the screen. In this case it will appear to the user that @@ -103,6 +113,9 @@ class TOCItem(QStandardItem): # be larger than 25, but that's a decent compromise. Also we dont want # to count a partial line as being visible. + # We only care about y position + anchor_map = {k:v[1] for k, v in anchor_map.iteritems()} + if spine_index >= self.starts_at and spine_index <= self.ends_at: # The position at which this anchor is present in the document start_pos = anchor_map.get(self.start_anchor, 0) @@ -115,7 +128,7 @@ class TOCItem(QStandardItem): # ancestors of this entry. psp = [anchor_map.get(x, 0) for x in self.possible_end_anchors] psp = [x for x in psp if x >= start_pos] - # The end position. The first anchor whose pos is >= self.start_pos + # The end position. The first anchor whose pos is >= start_pos # or if the end is not in this spine item, we set it to the bottom # of the window +1 end_pos = min(psp) if psp else (bottom+1 if self.ends_at >= @@ -141,6 +154,51 @@ class TOCItem(QStandardItem): if changed: self.setFont(self.bold_font if is_being_viewed else self.normal_font) + def update_indexing_state_paged(self, spine_index, viewport_rect, + anchor_map): + is_being_viewed = False + + left, right = viewport_rect[0], viewport_rect[2] + left, right = (left, 0), (right, -1) + + if spine_index >= self.starts_at and spine_index <= self.ends_at: + # The position at which this anchor is present in the document + start_pos = anchor_map.get(self.start_anchor, (0, 0)) + psp = [] + if self.ends_at == spine_index: + # Anchors that could possibly indicate the start of the next + # section and therefore the end of this section. + # self.possible_end_anchors is a set of anchors belonging to + # toc entries with depth <= self.depth that are also not + # ancestors of this entry. + psp = [anchor_map.get(x, (0, 0)) for x in self.possible_end_anchors] + psp = [x for x in psp if x >= start_pos] + # The end position. The first anchor whose pos is >= start_pos + # or if the end is not in this spine item, we set it to the column + # after the right edge of the viewport + end_pos = min(psp) if psp else (right if self.ends_at >= + spine_index else (0, 0)) + if spine_index > self.starts_at and spine_index < self.ends_at: + # The entire spine item is contained in this entry + is_being_viewed = True + elif (spine_index == self.starts_at and right > start_pos and + # This spine item contains the start + # The start position is before the end of the viewport + (spine_index != self.ends_at or left < end_pos)): + # The end position is after the start of the viewport + is_being_viewed = True + elif (spine_index == self.ends_at and left < end_pos and + # This spine item contains the end + # The end position is after the start of the viewport + (spine_index != self.starts_at or right > start_pos)): + # The start position is before the end of the viewport + is_being_viewed = True + + changed = is_being_viewed != self.is_being_viewed + self.is_being_viewed = is_being_viewed + if changed: + self.setFont(self.bold_font if is_being_viewed else self.normal_font) + def __repr__(self): return 'TOC Item: %s %s#%s'%(self.title, self.abspath, self.fragment) @@ -183,20 +241,26 @@ class TOC(QStandardItemModel): self.currently_viewed_entry = t return items_being_viewed - def next_entry(self, spine_pos, anchor_map, scroll_pos, backwards=False, - current_entry=None): + def next_entry(self, spine_pos, anchor_map, viewport_rect, in_paged_mode, + backwards=False, current_entry=None): current_entry = (self.currently_viewed_entry if current_entry is None else current_entry) if current_entry is None: return items = reversed(self.all_items) if backwards else self.all_items found = False - top = scroll_pos[0] + + if in_paged_mode: + start = viewport_rect[0] + anchor_map = {k:v[0] for k, v in anchor_map.iteritems()} + else: + start = viewport_rect[1] + anchor_map = {k:v[1] for k, v in anchor_map.iteritems()} + for item in items: if found: start_pos = anchor_map.get(item.start_anchor, 0) - if backwards and item.is_being_viewed and start_pos >= top: - # Going to this item will either not move the scroll - # position or cause to to *increase* instead of descresing + if backwards and item.is_being_viewed and start_pos >= start: + # This item will not cause any scrolling continue if item.starts_at != spine_pos or item.start_anchor: return item diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 4332bb651b..385884d008 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -6,14 +6,12 @@ Miscellaneous widgets used in the GUI import re, traceback, os from PyQt4.Qt import (QIcon, QFont, QLabel, QListWidget, QAction, - QListWidgetItem, QTextCharFormat, QApplication, - QSyntaxHighlighter, QCursor, QColor, QWidget, - QPixmap, QSplitterHandle, QToolButton, - QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, - QRegExp, QSettings, QSize, QSplitter, - QPainter, QLineEdit, QComboBox, QPen, QGraphicsScene, - QMenu, QStringListModel, QCompleter, QStringList, - QTimer, QRect, QFontDatabase, QGraphicsView) + QListWidgetItem, QTextCharFormat, QApplication, QSyntaxHighlighter, + QCursor, QColor, QWidget, QPixmap, QSplitterHandle, QToolButton, + QAbstractListModel, QVariant, Qt, SIGNAL, pyqtSignal, QRegExp, QSize, + QSplitter, QPainter, QLineEdit, QComboBox, QPen, QGraphicsScene, QMenu, + QStringListModel, QCompleter, QStringList, QTimer, QRect, + QFontDatabase, QGraphicsView, QByteArray) from calibre.constants import iswindows from calibre.gui2 import (NONE, error_dialog, pixmap_to_data, gprefs, @@ -803,69 +801,29 @@ class PythonHighlighter(QSyntaxHighlighter): # {{{ @classmethod def loadConfig(cls): Config = cls.Config - settings = QSettings() - def setDefaultString(name, default): - value = settings.value(name).toString() - if value.isEmpty(): - value = default - Config[name] = value for name in ("window", "shell"): - Config["%swidth" % name] = settings.value("%swidth" % name, - QVariant(QApplication.desktop() \ - .availableGeometry().width() / 2)).toInt()[0] - Config["%sheight" % name] = settings.value("%sheight" % name, - QVariant(QApplication.desktop() \ - .availableGeometry().height() / 2)).toInt()[0] - Config["%sy" % name] = settings.value("%sy" % name, - QVariant(0)).toInt()[0] - Config["toolbars"] = settings.value("toolbars").toByteArray() - Config["splitter"] = settings.value("splitter").toByteArray() - Config["shellx"] = settings.value("shellx", QVariant(0)).toInt()[0] - Config["windowx"] = settings.value("windowx", QVariant(QApplication \ - .desktop().availableGeometry().width() / 2)).toInt()[0] - Config["remembergeometry"] = settings.value("remembergeometry", - QVariant(True)).toBool() - Config["startwithshell"] = settings.value("startwithshell", - QVariant(True)).toBool() - Config["showwindowinfo"] = settings.value("showwindowinfo", - QVariant(True)).toBool() - setDefaultString("shellstartup", """\ - from __future__ import division - import codecs - import sys - sys.stdin = codecs.getreader("UTF8")(sys.stdin) - sys.stdout = codecs.getwriter("UTF8")(sys.stdout)""") - setDefaultString("newfile", """\ - #!/usr/bin/env python - - from __future__ import division - - import sys - """) - Config["backupsuffix"] = settings.value("backupsuffix", - QVariant(".bak")).toString() - setDefaultString("beforeinput", "#>>>") - setDefaultString("beforeoutput", "#---") - Config["cwd"] = settings.value("cwd", QVariant(".")).toString() - Config["tooltipsize"] = settings.value("tooltipsize", - QVariant(150)).toInt()[0] - Config["maxlinestoscan"] = settings.value("maxlinestoscan", - QVariant(5000)).toInt()[0] - Config["pythondocpath"] = settings.value("pythondocpath", - QVariant("http://docs.python.org")).toString() - Config["autohidefinddialog"] = settings.value("autohidefinddialog", - QVariant(True)).toBool() - Config["findcasesensitive"] = settings.value("findcasesensitive", - QVariant(False)).toBool() - Config["findwholewords"] = settings.value("findwholewords", - QVariant(False)).toBool() - Config["tabwidth"] = settings.value("tabwidth", - QVariant(4)).toInt()[0] - Config["fontfamily"] = settings.value("fontfamily", - QVariant("monospace")).toString() - Config["fontsize"] = settings.value("fontsize", - QVariant(10)).toInt()[0] + Config["%swidth" % name] = QVariant(QApplication.desktop().availableGeometry().width() / 2).toInt()[0] + Config["%sheight" % name] = QVariant(QApplication.desktop().availableGeometry().height() / 2).toInt()[0] + Config["%sy" % name] = QVariant(0).toInt()[0] + Config["toolbars"] = QByteArray(b'') + Config["splitter"] = QByteArray(b'') + Config["shellx"] = QVariant(0).toInt()[0] + Config["windowx"] = QVariant(QApplication.desktop().availableGeometry().width() / 2).toInt()[0] + Config["remembergeometry"] = QVariant(True).toBool() + Config["startwithshell"] = QVariant(True).toBool() + Config["showwindowinfo"] = QVariant(True).toBool() + Config["backupsuffix"] = QVariant(".bak").toString() + Config["cwd"] = QVariant(".").toString() + Config["tooltipsize"] = QVariant(150).toInt()[0] + Config["maxlinestoscan"] = QVariant(5000).toInt()[0] + Config["pythondocpath"] = QVariant("http://docs.python.org").toString() + Config["autohidefinddialog"] = QVariant(True).toBool() + Config["findcasesensitive"] = QVariant(False).toBool() + Config["findwholewords"] = QVariant(False).toBool() + Config["tabwidth"] = QVariant(4).toInt()[0] + Config["fontfamily"] = QVariant("monospace").toString() + Config["fontsize"] = QVariant(10).toInt()[0] for name, color, bold, italic in ( ("normal", "#000000", False, False), ("keyword", "#000080", True, False), @@ -877,12 +835,9 @@ class PythonHighlighter(QSyntaxHighlighter): # {{{ ("number", "#924900", False, False), ("error", "#FF0000", False, False), ("pyqt", "#50621A", False, False)): - Config["%sfontcolor" % name] = settings.value( - "%sfontcolor" % name, QVariant(color)).toString() - Config["%sfontbold" % name] = settings.value( - "%sfontbold" % name, QVariant(bold)).toBool() - Config["%sfontitalic" % name] = settings.value( - "%sfontitalic" % name, QVariant(italic)).toBool() + Config["%sfontcolor" % name] = QVariant(color).toString() + Config["%sfontbold" % name] = QVariant(bold).toBool() + Config["%sfontitalic" % name] = QVariant(italic).toBool() @classmethod diff --git a/src/calibre/utils/ipc/__init__.py b/src/calibre/utils/ipc/__init__.py index 93db2e9fc7..78191a225b 100644 --- a/src/calibre/utils/ipc/__init__.py +++ b/src/calibre/utils/ipc/__init__.py @@ -6,13 +6,22 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os +import os, errno from threading import Thread from calibre.constants import iswindows, get_windows_username ADDRESS = None +def eintr_retry_call(func, *args, **kwargs): + while True: + try: + return func(*args, **kwargs) + except EnvironmentError as e: + if getattr(e, 'errno', None) == errno.EINTR: + continue + raise + def gui_socket_address(): global ADDRESS if ADDRESS is None: diff --git a/src/calibre/utils/ipc/proxy.py b/src/calibre/utils/ipc/proxy.py index 3b037a42a6..897d79b423 100644 --- a/src/calibre/utils/ipc/proxy.py +++ b/src/calibre/utils/ipc/proxy.py @@ -15,6 +15,7 @@ from functools import partial from calibre import as_unicode, prints from calibre.constants import iswindows, DEBUG +from calibre.utils.ipc import eintr_retry_call def _encode(msg): raw = cPickle.dumps(msg, -1) @@ -68,7 +69,7 @@ class Writer(Thread): break try: self.data_written = True - self.conn.send_bytes(x) + eintr_retry_call(self.conn.send_bytes, x) except Exception as e: self.resultq.put(as_unicode(e)) else: @@ -112,7 +113,7 @@ class Server(Thread): def run(self): while self.keep_going: try: - conn = self.listener.accept() + conn = eintr_retry_call(self.listener.accept) self.handle_client(conn) except: pass @@ -125,7 +126,7 @@ class Server(Thread): def _handle_client(self, conn): while True: try: - func_name, args, kwargs = conn.recv() + func_name, args, kwargs = eintr_retry_call(conn.recv) except EOFError: try: conn.close() @@ -156,8 +157,8 @@ class Server(Thread): import traceback # Try to tell the client process what error happened try: - conn.send_bytes(_encode(('failed', (unicode(e), - as_unicode(traceback.format_exc()))))) + eintr_retry_call(conn.send_bytes, (_encode(('failed', (unicode(e), + as_unicode(traceback.format_exc())))))) except: pass raise diff --git a/src/calibre/utils/ipc/server.py b/src/calibre/utils/ipc/server.py index a4c5b5d881..2912971bf6 100644 --- a/src/calibre/utils/ipc/server.py +++ b/src/calibre/utils/ipc/server.py @@ -14,6 +14,7 @@ from multiprocessing.connection import Listener, arbitrary_address from collections import deque from binascii import hexlify +from calibre.utils.ipc import eintr_retry_call from calibre.utils.ipc.launch import Worker from calibre.utils.ipc.worker import PARALLEL_FUNCS from calibre import detect_ncpus as cpu_count @@ -38,7 +39,7 @@ class ConnectedWorker(Thread): def start_job(self, job): notification = PARALLEL_FUNCS[job.name][-1] is not None - self.conn.send((job.name, job.args, job.kwargs, job.description)) + eintr_retry_call(self.conn.send, (job.name, job.args, job.kwargs, job.description)) if notification: self.start() else: @@ -48,7 +49,7 @@ class ConnectedWorker(Thread): def run(self): while True: try: - x = self.conn.recv() + x = eintr_retry_call(self.conn.recv) self.notifications.put(x) except BaseException: break @@ -129,12 +130,7 @@ class Server(Thread): 'CALIBRE_WORKER_KEY' : hexlify(self.auth_key), 'CALIBRE_WORKER_RESULT' : hexlify(rfile.encode('utf-8')), } - for i in range(2): - # Try launch twice as occasionally on OS X - # Listener.accept fails with EINTR - cw = self.do_launch(env, gui, redirect_output, rfile) - if isinstance(cw, ConnectedWorker): - break + cw = self.do_launch(env, gui, redirect_output, rfile) if isinstance(cw, basestring): raise CriticalError('Failed to launch worker process:\n'+cw) if DEBUG: @@ -146,7 +142,7 @@ class Server(Thread): try: w(redirect_output=redirect_output) - conn = self.listener.accept() + conn = eintr_retry_call(self.listener.accept) if conn is None: raise Exception('Failed to launch worker process') except BaseException: diff --git a/src/calibre/utils/ipc/simple_worker.py b/src/calibre/utils/ipc/simple_worker.py index 68d879f8a8..ca84f9d84d 100644 --- a/src/calibre/utils/ipc/simple_worker.py +++ b/src/calibre/utils/ipc/simple_worker.py @@ -14,6 +14,7 @@ from threading import Thread from contextlib import closing from calibre.constants import iswindows +from calibre.utils.ipc import eintr_retry_call from calibre.utils.ipc.launch import Worker class WorkerError(Exception): @@ -35,30 +36,18 @@ class ConnectedWorker(Thread): def run(self): conn = tb = None - for i in range(2): - # On OS X an EINTR can interrupt the accept() call - try: - conn = self.listener.accept() - break - except: - tb = traceback.format_exc() - pass + try: + conn = eintr_retry_call(self.listener.accept) + except: + tb = traceback.format_exc() if conn is None: self.tb = tb return self.accepted = True with closing(conn): try: - try: - conn.send(self.args) - except: - # Maybe an EINTR - conn.send(self.args) - try: - self.res = conn.recv() - except: - # Maybe an EINTR - self.res = conn.recv() + eintr_retry_call(conn.send, self.args) + self.res = eintr_retry_call(conn.recv) except: self.tb = traceback.format_exc() @@ -202,11 +191,7 @@ def main(): address = cPickle.loads(unhexlify(os.environ['CALIBRE_WORKER_ADDRESS'])) key = unhexlify(os.environ['CALIBRE_WORKER_KEY']) with closing(Client(address, authkey=key)) as conn: - try: - args = conn.recv() - except: - # Maybe EINTR - args = conn.recv() + args = eintr_retry_call(conn.recv) try: mod, func, args, kwargs, module_is_source_code = args if module_is_source_code: diff --git a/src/calibre/utils/ipc/worker.py b/src/calibre/utils/ipc/worker.py index 0c43f00dca..08374400ac 100644 --- a/src/calibre/utils/ipc/worker.py +++ b/src/calibre/utils/ipc/worker.py @@ -16,6 +16,7 @@ from zipimport import ZipImportError from calibre import prints from calibre.constants import iswindows, isosx +from calibre.utils.ipc import eintr_retry_call PARALLEL_FUNCS = { 'lrfviewer' : @@ -75,7 +76,7 @@ class Progress(Thread): if x is None: break try: - self.conn.send(x) + eintr_retry_call(self.conn.send, x) except: break @@ -178,7 +179,7 @@ def main(): key = unhexlify(os.environ['CALIBRE_WORKER_KEY']) resultf = unhexlify(os.environ['CALIBRE_WORKER_RESULT']).decode('utf-8') with closing(Client(address, authkey=key)) as conn: - name, args, kwargs, desc = conn.recv() + name, args, kwargs, desc = eintr_retry_call(conn.recv) if desc: prints(desc) sys.stdout.flush() diff --git a/src/calibre/utils/magick/draw.py b/src/calibre/utils/magick/draw.py index e4163743d9..046d0d5224 100644 --- a/src/calibre/utils/magick/draw.py +++ b/src/calibre/utils/magick/draw.py @@ -245,12 +245,18 @@ class TextLine(object): def create_cover_page(top_lines, logo_path, width=590, height=750, - bgcolor='#ffffff', output_format='jpg'): + bgcolor='#ffffff', output_format='jpg', texture_data=None, + texture_opacity=1.0): ''' Create the standard calibre cover page and return it as a byte string in the specified output_format. ''' canvas = create_canvas(width, height, bgcolor) + if texture_data and hasattr(canvas, 'texture'): + texture = Image() + texture.load(texture_data) + texture.set_opacity(texture_opacity) + canvas.texture(texture) bottom = 10 for line in top_lines: @@ -263,7 +269,7 @@ def create_cover_page(top_lines, logo_path, width=590, height=750, if not foot_font: foot_font = P('fonts/liberation/LiberationMono-Regular.ttf') vanity = create_text_arc(__appname__ + ' ' + __version__, 24, - font=foot_font) + font=foot_font, bgcolor='#00000000') lwidth, lheight = vanity.size left = int(max(0, (width - lwidth)/2.)) top = height - lheight - 10 @@ -279,6 +285,9 @@ def create_cover_page(top_lines, logo_path, width=590, height=750, logo.size = (lwidth, lheight) left = int(max(0, (width - lwidth)/2.)) top = bottom+10 + extra = int((available[1] - lheight)/2.0) + if extra > 0: + top += extra canvas.compose(logo, left, top) return canvas.export(output_format) diff --git a/src/calibre/utils/magick/magick.c b/src/calibre/utils/magick/magick.c index e14c966282..4b336a2531 100644 --- a/src/calibre/utils/magick/magick.c +++ b/src/calibre/utils/magick/magick.c @@ -495,6 +495,7 @@ typedef struct { // Method declarations {{{ static PyObject* magick_Image_compose(magick_Image *self, PyObject *args, PyObject *kwargs); static PyObject* magick_Image_copy(magick_Image *self, PyObject *args, PyObject *kwargs); +static PyObject* magick_Image_texture(magick_Image *self, PyObject *args, PyObject *kwargs); // }}} static void @@ -926,7 +927,6 @@ magick_Image_flip(magick_Image *self, PyObject *args, PyObject *kwargs) { } // }}} - // Image.set_page {{{ static PyObject * @@ -1114,6 +1114,22 @@ magick_Image_destroy(magick_Image *self, PyObject *args, PyObject *kwargs) { } // }}} +// Image.set_opacity {{{ + +static PyObject * +magick_Image_set_opacity(magick_Image *self, PyObject *args, PyObject *kwargs) { + double opacity; + NULL_CHECK(NULL) + + + if (!PyArg_ParseTuple(args, "d", &opacity)) return NULL; + + if (!MagickSetImageOpacity(self->wand, opacity)) return magick_set_exception(self->wand); + + Py_RETURN_NONE; +} +// }}} + // Image attr list {{{ static PyMethodDef magick_Image_methods[] = { {"destroy", (PyCFunction)magick_Image_destroy, METH_VARARGS, @@ -1145,6 +1161,14 @@ static PyMethodDef magick_Image_methods[] = { "compose(img, left, top, op) \n\n Compose img using operation op at (left, top)" }, + {"texture", (PyCFunction)magick_Image_texture, METH_VARARGS, + "texture(img)) \n\n Repeatedly tile img across and down the canvas." + }, + + {"set_opacity", (PyCFunction)magick_Image_set_opacity, METH_VARARGS, + "set_opacity(opacity)) \n\n Set the opacity of this image (between 0.0 - transparent and 1.0 - opaque)" + }, + {"copy", (PyCFunction)magick_Image_copy, METH_VARARGS, "copy(img) \n\n Copy img to self." }, @@ -1335,6 +1359,23 @@ magick_Image_copy(magick_Image *self, PyObject *args, PyObject *kwargs) } // }}} +// Image.texture {{{ +static PyObject * +magick_Image_texture(magick_Image *self, PyObject *args, PyObject *kwargs) { + PyObject *img; + magick_Image *texture; + + NULL_CHECK(NULL) + + if (!PyArg_ParseTuple(args, "O!", &magick_ImageType, &img)) return NULL; + texture = (magick_Image*)img; + if (!IsMagickWand(texture->wand)) {PyErr_SetString(PyExc_TypeError, "Not a valid ImageMagick wand"); return NULL;} + + self->wand = MagickTextureImage(self->wand, texture->wand); + + Py_RETURN_NONE; +} + // }}} // Module functions {{{ diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index 4a8d980fee..2a49804422 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -77,7 +77,8 @@ class BasicNewsRecipe(Recipe): delay = 0 #: Publication type - #: Set to newspaper, magazine or blog + #: Set to newspaper, magazine or blog. If set to None, no publication type + #: metadata will be written to the opf file. publication_type = 'unknown' #: Number of simultaneous downloads. Set to 1 if the server is picky. @@ -1264,7 +1265,8 @@ class BasicNewsRecipe(Recipe): mi = MetaInformation(title, [__appname__]) mi.publisher = __appname__ mi.author_sort = __appname__ - mi.publication_type = 'periodical:'+self.publication_type+':'+self.short_title() + if self.publication_type: + mi.publication_type = 'periodical:'+self.publication_type+':'+self.short_title() mi.timestamp = nowf() article_titles, aseen = [], set() for f in feeds: diff --git a/src/calibre/web/fetch/simple.py b/src/calibre/web/fetch/simple.py index b8809147aa..d79ef6204a 100644 --- a/src/calibre/web/fetch/simple.py +++ b/src/calibre/web/fetch/simple.py @@ -12,6 +12,7 @@ from urllib import url2pathname, quote from httplib import responses from PIL import Image from cStringIO import StringIO +from base64 import b64decode from calibre import browser, relpath, unicode_path from calibre.constants import filesystem_encoding, iswindows @@ -346,22 +347,29 @@ class RecursiveFetcher(object): c = 0 for tag in soup.findAll(lambda tag: tag.name.lower()=='img' and tag.has_key('src')): iurl = tag['src'] - if callable(self.image_url_processor): - iurl = self.image_url_processor(baseurl, iurl) - if not urlparse.urlsplit(iurl).scheme: - iurl = urlparse.urljoin(baseurl, iurl, False) - with self.imagemap_lock: - if self.imagemap.has_key(iurl): - tag['src'] = self.imagemap[iurl] + if iurl.startswith('data:image/'): + try: + data = b64decode(iurl.partition(',')[-1]) + except: + self.log.exception('Failed to decode embedded image') continue - try: - data = self.fetch_url(iurl) - if data == 'GIF89a\x01': - # Skip empty GIF files as PIL errors on them anyway + else: + if callable(self.image_url_processor): + iurl = self.image_url_processor(baseurl, iurl) + if not urlparse.urlsplit(iurl).scheme: + iurl = urlparse.urljoin(baseurl, iurl, False) + with self.imagemap_lock: + if self.imagemap.has_key(iurl): + tag['src'] = self.imagemap[iurl] + continue + try: + data = self.fetch_url(iurl) + if data == 'GIF89a\x01': + # Skip empty GIF files as PIL errors on them anyway + continue + except Exception: + self.log.exception('Could not fetch image ', iurl) continue - except Exception: - self.log.exception('Could not fetch image ', iurl) - continue c += 1 fname = ascii_filename('img'+str(c)) if isinstance(fname, unicode): diff --git a/src/qtcurve/style/qtcurve.cpp b/src/qtcurve/style/qtcurve.cpp index dcb24f00f1..b48eb9d421 100644 --- a/src/qtcurve/style/qtcurve.cpp +++ b/src/qtcurve/style/qtcurve.cpp @@ -1075,9 +1075,7 @@ void Style::init(bool initial) #endif } - opts.contrast=QSettings(QLatin1String("Trolltech")).value("/Qt/KDE/contrast", DEFAULT_CONTRAST).toInt(); - if(opts.contrast<0 || opts.contrast>10) - opts.contrast=DEFAULT_CONTRAST; + opts.contrast=DEFAULT_CONTRAST; //Changed by Kovid shadeColors(QApplication::palette().color(QPalette::Active, QPalette::Highlight), itsHighlightCols); shadeColors(QApplication::palette().color(QPalette::Active, QPalette::Background), itsBackgroundCols); @@ -1522,7 +1520,7 @@ void Style::polish(QApplication *app) void Style::polish(QPalette &palette) { - int contrast(QSettings(QLatin1String("Trolltech")).value("/Qt/KDE/contrast", DEFAULT_CONTRAST).toInt()); + int contrast = DEFAULT_CONTRAST; // Changed by Kovid bool newContrast(false); if(contrast<0 || contrast>10)