Merge from trunk

This commit is contained in:
Charles Haley 2011-03-07 11:57:05 +00:00
commit 2b92479e13
13 changed files with 777 additions and 413 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 654 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

View File

@ -4,6 +4,7 @@
# #
# #
# copyright 2002 Paul Henry Tremblay #
# Copyright 2011 Kovid Goyal
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
@ -19,21 +20,21 @@
#########################################################################
-->
<xsl:output method="xml" encoding="UTF-8"/>
<xsl:key name="note-link" match="fb:section" use="@id"/>
<xsl:template match="/*">
<html>
<head>
<xsl:if test="fb:description/fb:title-info/fb:lang = 'ru'">
<meta HTTP-EQUIV="content-type" CONTENT="text/html; charset=UTF-8"/>
</xsl:if>
<title>
<xsl:value-of select="fb:description/fb:title-info/fb:book-title"/>
</title>
<style type="text/css">
<xsl:output method="xml" encoding="UTF-8"/>
<xsl:key name="note-link" match="fb:section" use="@id"/>
<xsl:template match="/*">
<html>
<head>
<xsl:if test="fb:description/fb:title-info/fb:lang = 'ru'">
<meta HTTP-EQUIV="content-type" CONTENT="text/html; charset=UTF-8"/>
</xsl:if>
<title>
<xsl:value-of select="fb:description/fb:title-info/fb:book-title"/>
</title>
<style type="text/css">
a { color : #0002CC }
a:hover { color : #BF0000 }
a:hover { color : #BF0000 }
body { background-color : #FEFEFE; color : #000000; font-family : Verdana, Geneva, Arial, Helvetica, sans-serif; text-align : justify }
@ -62,90 +63,90 @@
.epigraph{width:50%; margin-left : 35%;}
div.paragraph { text-align: justify; text-indent: 2em; }
</style>
</style>
<link rel="stylesheet" type="text/css" href="inline-styles.css" />
</head>
<body>
<xsl:for-each select="fb:description/fb:title-info/fb:annotation">
<div>
<xsl:call-template name="annotation"/>
</div>
<hr/>
</xsl:for-each>
<!-- BUILD TOC -->
<ul>
<xsl:apply-templates select="fb:body" mode="toc"/>
</ul>
<hr/>
</head>
<body>
<xsl:for-each select="fb:description/fb:title-info/fb:annotation">
<div>
<xsl:call-template name="annotation"/>
</div>
<hr/>
</xsl:for-each>
<!-- BUILD TOC -->
<ul>
<xsl:apply-templates select="fb:body" mode="toc"/>
</ul>
<hr/>
<!-- END BUILD TOC -->
<!-- BUILD BOOK -->
<xsl:for-each select="fb:body">
<xsl:if test="position()!=1">
<hr/>
</xsl:if>
<xsl:if test="@name">
<h4 align="center">
<xsl:value-of select="@name"/>
</h4>
</xsl:if>
<!-- <xsl:apply-templates /> -->
<xsl:apply-templates/>
</xsl:for-each>
</body>
</html>
</xsl:template>
<!-- author template -->
<xsl:template name="author">
<xsl:value-of select="fb:first-name"/>
<xsl:text disable-output-escaping="no">&#032;</xsl:text>
<xsl:value-of select="fb:middle-name"/>&#032;
<!-- BUILD BOOK -->
<xsl:for-each select="fb:body">
<xsl:if test="position()!=1">
<hr/>
</xsl:if>
<xsl:if test="@name">
<h4 align="center">
<xsl:value-of select="@name"/>
</h4>
</xsl:if>
<!-- <xsl:apply-templates /> -->
<xsl:apply-templates/>
</xsl:for-each>
</body>
</html>
</xsl:template>
<!-- author template -->
<xsl:template name="author">
<xsl:value-of select="fb:first-name"/>
<xsl:text disable-output-escaping="no">&#032;</xsl:text>
<xsl:value-of select="fb:middle-name"/>&#032;
<xsl:text disable-output-escaping="no">&#032;</xsl:text>
<xsl:value-of select="fb:last-name"/>
<br/>
</xsl:template>
<!-- secuence template -->
<xsl:template name="sequence">
<LI/>
<xsl:value-of select="@name"/>
<xsl:if test="@number">
<xsl:text disable-output-escaping="no">,&#032;#</xsl:text>
<xsl:value-of select="@number"/>
</xsl:if>
<xsl:if test="fb:sequence">
<ul>
<xsl:for-each select="fb:sequence">
<xsl:call-template name="sequence"/>
</xsl:for-each>
</ul>
</xsl:if>
<!-- <br/> -->
</xsl:template>
<!-- toc template -->
<xsl:template match="fb:section|fb:body" mode="toc">
<xsl:choose>
<xsl:when test="name()='body' and position()=1 and not(fb:title)">
<xsl:apply-templates select="fb:section" mode="toc"/>
</xsl:when>
<xsl:otherwise>
<li>
<a href="#TOC_{generate-id()}"><xsl:value-of select="normalize-space(fb:title/fb:p[1] | @name)"/></a>
<xsl:if test="fb:section">
<ul><xsl:apply-templates select="fb:section" mode="toc"/></ul>
</xsl:if>
</li>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- description -->
<xsl:template match="fb:description">
<xsl:apply-templates/>
</xsl:template>
<!-- body -->
<xsl:template match="fb:body">
<div><xsl:apply-templates/></div>
</xsl:template>
<xsl:value-of select="fb:last-name"/>
<br/>
</xsl:template>
<!-- secuence template -->
<xsl:template name="sequence">
<LI/>
<xsl:value-of select="@name"/>
<xsl:if test="@number">
<xsl:text disable-output-escaping="no">,&#032;#</xsl:text>
<xsl:value-of select="@number"/>
</xsl:if>
<xsl:if test="fb:sequence">
<ul>
<xsl:for-each select="fb:sequence">
<xsl:call-template name="sequence"/>
</xsl:for-each>
</ul>
</xsl:if>
<!-- <br/> -->
</xsl:template>
<!-- toc template -->
<xsl:template match="fb:section|fb:body" mode="toc">
<xsl:choose>
<xsl:when test="name()='body' and position()=1 and not(fb:title)">
<xsl:apply-templates select="fb:section" mode="toc"/>
</xsl:when>
<xsl:otherwise>
<li>
<a href="#TOC_{generate-id()}"><xsl:value-of select="normalize-space(fb:title/fb:p[1] | @name)"/></a>
<xsl:if test="fb:section">
<ul><xsl:apply-templates select="fb:section" mode="toc"/></ul>
</xsl:if>
</li>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- description -->
<xsl:template match="fb:description">
<xsl:apply-templates/>
</xsl:template>
<!-- body -->
<xsl:template match="fb:body">
<div><xsl:apply-templates/></div>
</xsl:template>
<xsl:template match="fb:section">
<xsl:template match="fb:section">
<xsl:variable name="section_has_title">
<xsl:choose>
<xsl:when test="./fb:title"><xsl:value-of select="generate-id()" /></xsl:when>
@ -164,15 +165,15 @@
<xsl:apply-templates>
<xsl:with-param name="section_toc_id" select="$section_has_title" />
</xsl:apply-templates>
</xsl:template>
<!-- section/title -->
<xsl:template match="fb:section/fb:title|fb:poem/fb:title">
</xsl:template>
<!-- section/title -->
<xsl:template match="fb:section/fb:title|fb:poem/fb:title">
<xsl:param name="section_toc_id" />
<xsl:choose>
<xsl:when test="count(ancestor::node()) &lt; 9">
<xsl:element name="{concat('h',count(ancestor::node())-3)}">
<xsl:choose>
<xsl:when test="count(ancestor::node()) &lt; 9">
<xsl:element name="{concat('h',count(ancestor::node())-3)}">
<xsl:if test="../@id">
<xsl:attribute name="id"><xsl:value-of select="../@id" /></xsl:attribute>
</xsl:if>
@ -181,79 +182,79 @@
<xsl:attribute name="id">TOC_<xsl:value-of select="$section_toc_id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<a name="TOC_{generate-id()}"></a>
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="id"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:element name="h6">
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="id"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- section/title -->
<xsl:template match="fb:body/fb:title">
<a name="TOC_{generate-id()}"></a>
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="id"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</xsl:element>
</xsl:when>
<xsl:otherwise>
<xsl:element name="h6">
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="id"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</xsl:element>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- section/title -->
<xsl:template match="fb:body/fb:title">
<xsl:element name="h1">
<xsl:apply-templates />
</xsl:element>
</xsl:template>
</xsl:template>
<xsl:template match="fb:title/fb:p">
<xsl:apply-templates/><xsl:text disable-output-escaping="no">&#032;</xsl:text><br/>
</xsl:template>
<!-- subtitle -->
<xsl:template match="fb:subtitle">
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<h5>
<xsl:apply-templates/>
</h5>
</xsl:template>
<!-- p -->
<xsl:template match="fb:p">
<xsl:template match="fb:title/fb:p">
<xsl:apply-templates/><xsl:text disable-output-escaping="no">&#032;</xsl:text><br/>
</xsl:template>
<!-- subtitle -->
<xsl:template match="fb:subtitle">
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<h5>
<xsl:apply-templates/>
</h5>
</xsl:template>
<!-- p -->
<xsl:template match="fb:p">
<xsl:element name="div">
<xsl:attribute name="class">paragraph</xsl:attribute>
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:if test="@style">
<xsl:attribute name="style"><xsl:value-of select="@style"/></xsl:attribute>
</xsl:if>
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
<!-- strong -->
<xsl:template match="fb:strong">
<b><xsl:apply-templates/></b>
</xsl:template>
<!-- emphasis -->
<xsl:template match="fb:emphasis">
<i> <xsl:apply-templates/></i>
</xsl:template>
<!-- style -->
<xsl:template match="fb:style">
<span class="{@name}"><xsl:apply-templates/></span>
</xsl:template>
<!-- empty-line -->
<xsl:template match="fb:empty-line">
<br/>
</xsl:template>
</xsl:template>
<!-- strong -->
<xsl:template match="fb:strong">
<b><xsl:apply-templates/></b>
</xsl:template>
<!-- emphasis -->
<xsl:template match="fb:emphasis">
<i> <xsl:apply-templates/></i>
</xsl:template>
<!-- style -->
<xsl:template match="fb:style">
<span class="{@name}"><xsl:apply-templates/></span>
</xsl:template>
<!-- empty-line -->
<xsl:template match="fb:empty-line">
<br/>
</xsl:template>
<!-- super/sub-scripts -->
<xsl:template match="fb:sup">
<sup><xsl:apply-templates/></sup>
@ -261,123 +262,140 @@
<xsl:template match="fb:sub">
<sub><xsl:apply-templates/></sub>
</xsl:template>
<!-- link -->
<xsl:template match="fb:a">
<xsl:element name="a">
<xsl:attribute name="href"><xsl:value-of select="@xlink:href"/></xsl:attribute>
<xsl:attribute name="title">
<xsl:choose>
<xsl:when test="starts-with(@xlink:href,'#')"><xsl:value-of select="key('note-link',substring-after(@xlink:href,'#'))/fb:p"/></xsl:when>
<xsl:otherwise><xsl:value-of select="key('note-link',@xlink:href)/fb:p"/></xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:choose>
<xsl:when test="(@type) = 'note'">
<sup>
<xsl:apply-templates/>
</sup>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates/>
</xsl:otherwise>
</xsl:choose>
</xsl:element>
</xsl:template>
<!-- annotation -->
<xsl:template name="annotation">
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<h3>Annotation</h3>
<xsl:apply-templates/>
</xsl:template>
<!-- epigraph -->
<xsl:template match="fb:epigraph">
<blockquote class="epigraph">
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</blockquote>
</xsl:template>
<!-- epigraph/text-author -->
<xsl:template match="fb:epigraph/fb:text-author">
<blockquote>
<i><xsl:apply-templates/></i>
</blockquote>
</xsl:template>
<!-- cite -->
<xsl:template match="fb:cite">
<blockquote>
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</blockquote>
</xsl:template>
<!-- cite/text-author -->
<xsl:template match="fb:text-author">
<blockquote>
<i> <xsl:apply-templates/></i></blockquote>
</xsl:template>
<!-- date -->
<xsl:template match="fb:date">
<xsl:choose>
<xsl:when test="not(@value)">
&#160;&#160;&#160;<xsl:apply-templates/>
<br/>
</xsl:when>
<xsl:otherwise>
&#160;&#160;&#160;<xsl:value-of select="@value"/>
<br/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- poem -->
<xsl:template match="fb:poem">
<blockquote>
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</blockquote>
</xsl:template>
<!-- link -->
<xsl:template match="fb:a">
<xsl:element name="a">
<xsl:attribute name="href"><xsl:value-of select="@xlink:href"/></xsl:attribute>
<xsl:attribute name="title">
<xsl:choose>
<xsl:when test="starts-with(@xlink:href,'#')"><xsl:value-of select="key('note-link',substring-after(@xlink:href,'#'))/fb:p"/></xsl:when>
<xsl:otherwise><xsl:value-of select="key('note-link',@xlink:href)/fb:p"/></xsl:otherwise>
</xsl:choose>
</xsl:attribute>
<xsl:choose>
<xsl:when test="(@type) = 'note'">
<sup>
<xsl:apply-templates/>
</sup>
</xsl:when>
<xsl:otherwise>
<xsl:apply-templates/>
</xsl:otherwise>
</xsl:choose>
</xsl:element>
</xsl:template>
<!-- annotation -->
<xsl:template name="annotation">
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<h3>Annotation</h3>
<xsl:apply-templates/>
</xsl:template>
<!-- tables -->
<xsl:template match="fb:table">
<table>
<xsl:apply-templates/>
</table>
</xsl:template>
<xsl:template match="fb:tr">
<tr><xsl:apply-templates/></tr>
</xsl:template>
<xsl:template match="fb:td">
<xsl:element name="td">
<xsl:if test="@align">
<xsl:attribute name="align"><xsl:value-of select="@align"/></xsl:attribute>
</xsl:if>
<xsl:apply-templates/>
</xsl:element>
</xsl:template>
<!-- epigraph -->
<xsl:template match="fb:epigraph">
<blockquote class="epigraph">
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</blockquote>
</xsl:template>
<!-- epigraph/text-author -->
<xsl:template match="fb:epigraph/fb:text-author">
<blockquote>
<i><xsl:apply-templates/></i>
</blockquote>
</xsl:template>
<!-- cite -->
<xsl:template match="fb:cite">
<blockquote>
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</blockquote>
</xsl:template>
<!-- cite/text-author -->
<xsl:template match="fb:text-author">
<blockquote>
<i> <xsl:apply-templates/></i></blockquote>
</xsl:template>
<!-- date -->
<xsl:template match="fb:date">
<xsl:choose>
<xsl:when test="not(@value)">
&#160;&#160;&#160;<xsl:apply-templates/>
<br/>
</xsl:when>
<xsl:otherwise>
&#160;&#160;&#160;<xsl:value-of select="@value"/>
<br/>
</xsl:otherwise>
</xsl:choose>
</xsl:template>
<!-- poem -->
<xsl:template match="fb:poem">
<blockquote>
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/>
</blockquote>
</xsl:template>
<!-- stanza -->
<xsl:template match="fb:stanza">
<xsl:apply-templates/>
<br/>
</xsl:template>
<!-- v -->
<xsl:template match="fb:v">
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/><br/>
</xsl:template>
<!-- image -->
<xsl:template match="fb:image">
<div align="center">
<img border="1">
<xsl:choose>
<xsl:when test="starts-with(@xlink:href,'#')">
<xsl:attribute name="src"><xsl:value-of select="substring-after(@xlink:href,'#')"/></xsl:attribute>
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="src"><xsl:value-of select="@xlink:href"/></xsl:attribute>
</xsl:otherwise>
</xsl:choose>
</img>
</div>
</xsl:template>
<!-- stanza -->
<xsl:template match="fb:stanza">
<xsl:apply-templates/>
<br/>
</xsl:template>
<!-- v -->
<xsl:template match="fb:v">
<xsl:if test="@id">
<xsl:element name="a">
<xsl:attribute name="name"><xsl:value-of select="@id"/></xsl:attribute>
</xsl:element>
</xsl:if>
<xsl:apply-templates/><br/>
</xsl:template>
<!-- image -->
<xsl:template match="fb:image">
<div align="center">
<img border="1">
<xsl:choose>
<xsl:when test="starts-with(@xlink:href,'#')">
<xsl:attribute name="src"><xsl:value-of select="substring-after(@xlink:href,'#')"/></xsl:attribute>
</xsl:when>
<xsl:otherwise>
<xsl:attribute name="src"><xsl:value-of select="@xlink:href"/></xsl:attribute>
</xsl:otherwise>
</xsl:choose>
</img>
</div>
</xsl:template>
</xsl:stylesheet>

View File

@ -61,8 +61,9 @@ def osx_version():
if m:
return int(m.group(1)), int(m.group(2)), int(m.group(3))
_filename_sanitize = re.compile(r'[\xae\0\\|\?\*<":>\+/]')
_filename_sanitize_unicode = frozenset([u'\\', u'|', u'?', u'*', u'<',
u'"', u':', u'>', u'+', u'/'] + list(map(unichr, xrange(32))))
def sanitize_file_name(name, substitute='_', as_unicode=False):
'''
@ -83,8 +84,35 @@ def sanitize_file_name(name, substitute='_', as_unicode=False):
one = one.decode(filesystem_encoding)
one = one.replace('..', substitute)
# Windows doesn't like path components that end with a period
if one.endswith('.'):
if one and one[-1] in ('.', ' '):
one = one[:-1]+'_'
# Names starting with a period are hidden on Unix
if one.startswith('.'):
one = '_' + one[1:]
return one
def sanitize_file_name_unicode(name, substitute='_'):
'''
Sanitize the filename `name`. All invalid characters are replaced by `substitute`.
The set of invalid characters is the union of the invalid characters in Windows,
OS X and Linux. Also removes leading and trailing whitespace.
**WARNING:** This function also replaces path separators, so only pass file names
and not full paths to it.
'''
if not isinstance(name, unicode):
return sanitize_file_name(name, substitute=substitute, as_unicode=True)
chars = [substitute if c in _filename_sanitize_unicode else c for c in
name]
one = u''.join(chars)
one = re.sub(r'\s', ' ', one).strip()
one = re.sub(r'^\.+$', '_', one)
one = one.replace('..', substitute)
# Windows doesn't like path components that end with a period or space
if one and one[-1] in ('.', ' '):
one = one[:-1]+'_'
# Names starting with a period are hidden on Unix
if one.startswith('.'):
one = '_' + one[1:]
return one

View File

@ -204,15 +204,29 @@ class AddAction(InterfaceAction):
to_device = self.gui.stack.currentIndex() != 0
self._add_books(paths, to_device)
def files_dropped_on_book(self, event, paths):
def remote_file_dropped_on_book(self, url, fname):
if self.gui.current_view() is not self.gui.library_view:
return
db = self.gui.library_view.model().db
current_idx = self.gui.library_view.currentIndex()
if not current_idx.isValid(): return
cid = db.id(current_idx.row())
from calibre.gui2.dnd import DownloadDialog
d = DownloadDialog(url, fname, self.gui)
d.start_download()
if d.err is None:
self.files_dropped_on_book(None, [d.fpath], cid=cid)
def files_dropped_on_book(self, event, paths, cid=None):
accept = False
if self.gui.current_view() is not self.gui.library_view:
return
db = self.gui.library_view.model().db
cover_changed = False
current_idx = self.gui.library_view.currentIndex()
if not current_idx.isValid(): return
cid = db.id(current_idx.row())
if cid is None:
if not current_idx.isValid(): return
cid = db.id(current_idx.row()) if cid is None else cid
for path in paths:
ext = os.path.splitext(path)[1].lower()
if ext:
@ -227,8 +241,9 @@ class AddAction(InterfaceAction):
elif ext in BOOK_EXTENSIONS:
db.add_format_with_hooks(cid, ext, path, index_is_id=True)
accept = True
if accept:
if accept and event is not None:
event.accept()
if current_idx.isValid():
self.gui.library_view.model().current_changed(current_idx, current_idx)
if cover_changed:
if self.gui.cover_flow:

View File

@ -5,7 +5,7 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, collections, sys
import collections, sys
from Queue import Queue
from PyQt4.Qt import QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, \
@ -14,7 +14,8 @@ from PyQt4.Qt import QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, \
from PyQt4.QtWebKit import QWebView
from calibre import fit_image, prepare_string_for_xml
from calibre.gui2.widgets import IMAGE_EXTENSIONS
from calibre.gui2.dnd import dnd_has_image, dnd_get_image, dnd_get_files, \
IMAGE_EXTENSIONS, dnd_has_extension
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.constants import preferred_encoding
from calibre.library.comments import comments_to_html
@ -165,11 +166,12 @@ class CoverView(QWidget): # {{{
def copy_to_clipboard(self):
QApplication.instance().clipboard().setPixmap(self.pixmap)
def paste_from_clipboard(self):
cb = QApplication.instance().clipboard()
pmap = cb.pixmap()
if pmap.isNull() and cb.supportsSelection():
pmap = cb.pixmap(cb.Selection)
def paste_from_clipboard(self, pmap=None):
if not isinstance(pmap, QPixmap):
cb = QApplication.instance().clipboard()
pmap = cb.pixmap()
if pmap.isNull() and cb.supportsSelection():
pmap = cb.pixmap(cb.Selection)
if not pmap.isNull():
self.pixmap = pmap
self.do_layout()
@ -226,6 +228,7 @@ class BookInfo(QWebView):
self._link_clicked = False
self.setAttribute(Qt.WA_OpaquePaintEvent, False)
palette = self.palette()
self.setAcceptDrops(False)
palette.setBrush(QPalette.Base, Qt.transparent)
self.page().setPalette(palette)
@ -388,36 +391,50 @@ class BookDetails(QWidget): # {{{
show_book_info = pyqtSignal()
open_containing_folder = pyqtSignal(int)
view_specific_format = pyqtSignal(int, object)
# Drag 'n drop {{{
DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS+BOOK_EXTENSIONS
remote_file_dropped = pyqtSignal(object, object)
files_dropped = pyqtSignal(object, object)
cover_changed = pyqtSignal(object, object)
# application/x-moz-file-promise-url
@classmethod
def paths_from_event(cls, event):
'''
Accept a drop event and return a list of paths that can be read from
and represent files with extensions.
'''
if event.mimeData().hasFormat('text/uri-list'):
urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
urls = [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
return [u for u in urls if os.path.splitext(u)[1][1:].lower() in cls.DROPABBLE_EXTENSIONS]
# Drag 'n drop {{{
DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS+BOOK_EXTENSIONS
def dragEnterEvent(self, event):
if int(event.possibleActions() & Qt.CopyAction) + \
int(event.possibleActions() & Qt.MoveAction) == 0:
return
paths = self.paths_from_event(event)
if paths:
md = event.mimeData()
if dnd_has_extension(md, self.DROPABBLE_EXTENSIONS) or \
dnd_has_image(md):
event.acceptProposedAction()
def dropEvent(self, event):
paths = self.paths_from_event(event)
event.setDropAction(Qt.CopyAction)
self.files_dropped.emit(event, paths)
md = event.mimeData()
x, y = dnd_get_image(md)
if x is not None:
# We have an image, set cover
event.accept()
if y is None:
# Local image
self.cover_view.paste_from_clipboard(x)
else:
self.remote_file_dropped.emit(x, y)
# We do not support setting cover *and* adding formats for
# a remote drop, anyway, so return
return
# Now look for ebook files
urls, filenames = dnd_get_files(md, BOOK_EXTENSIONS)
if not urls:
# Nothing found
return
if not filenames:
# Local files
self.files_dropped.emit(event, urls)
else:
# Remote files, use the first file
self.remote_file_dropped.emit(urls[0], filenames[0])
event.accept()
def dragMoveEvent(self, event):
event.acceptProposedAction()

View File

@ -43,6 +43,9 @@
<height>0</height>
</size>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
<property name="minimumContentsLength">
<number>30</number>
</property>

325
src/calibre/gui2/dnd.py Normal file
View File

@ -0,0 +1,325 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import posixpath, os, urllib, re
from urlparse import urlparse, urlunparse
from threading import Thread
from Queue import Queue, Empty
from PyQt4.Qt import QPixmap, Qt, QDialog, QLabel, QVBoxLayout, \
QDialogButtonBox, QProgressBar, QTimer
from calibre.constants import DEBUG, iswindows
from calibre.ptempfile import PersistentTemporaryFile
from calibre import browser, as_unicode, prints
from calibre.gui2 import error_dialog
IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'gif', 'png', 'bmp']
class Worker(Thread): # {{{
def __init__(self, url, fpath, rq):
Thread.__init__(self)
self.url, self.fpath = url, fpath
self.daemon = True
self.rq = rq
self.err = self.tb = None
def run(self):
try:
br = browser()
br.retrieve(self.url, self.fpath, self.callback)
except Exception, e:
self.err = as_unicode(e)
import traceback
self.tb = traceback.format_exc()
def callback(self, a, b, c):
self.rq.put((a, b, c))
# }}}
class DownloadDialog(QDialog): # {{{
def __init__(self, url, fname, parent):
QDialog.__init__(self, parent)
self.setWindowTitle(_('Download %s')%fname)
self.l = QVBoxLayout(self)
self.purl = urlparse(url)
self.msg = QLabel(_('Downloading <b>%s</b> from %s')%(fname,
self.purl.netloc))
self.msg.setWordWrap(True)
self.l.addWidget(self.msg)
self.pb = QProgressBar(self)
self.pb.setMinimum(0)
self.pb.setMaximum(0)
self.l.addWidget(self.pb)
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel, Qt.Horizontal, self)
self.l.addWidget(self.bb)
self.bb.rejected.connect(self.reject)
sz = self.sizeHint()
self.resize(max(sz.width(), 400), sz.height())
fpath = PersistentTemporaryFile(os.path.splitext(fname)[1])
fpath.close()
self.fpath = fpath.name
self.worker = Worker(url, self.fpath, Queue())
self.rejected = False
def reject(self):
self.rejected = True
QDialog.reject(self)
def start_download(self):
self.worker.start()
QTimer.singleShot(50, self.update)
self.exec_()
if self.worker.err is not None:
error_dialog(self.parent(), _('Download failed'),
_('Failed to download from %r with error: %s')%(
self.worker.url, self.worker.err),
det_msg=self.worker.tb, show=True)
def update(self):
if self.rejected:
return
try:
progress = self.worker.rq.get_nowait()
except Empty:
pass
else:
self.update_pb(progress)
if not self.worker.is_alive():
return self.accept()
QTimer.singleShot(50, self.update)
def update_pb(self, progress):
transferred, block_size, total = progress
if total == -1:
self.pb.setMaximum(0)
self.pb.setMinimum(0)
self.pb.setValue(0)
else:
so_far = transferred * block_size
self.pb.setMaximum(max(total, so_far))
self.pb.setValue(so_far)
@property
def err(self):
return self.worker.err
# }}}
def dnd_has_image(md):
return md.hasImage()
def data_as_string(f, md):
raw = bytes(md.data(f))
if '/x-moz' in f:
try:
raw = raw.decode('utf-16')
except:
pass
return raw
def dnd_has_extension(md, extensions):
if DEBUG:
prints('Debugging DND event')
for f in md.formats():
f = unicode(f)
prints(f, repr(data_as_string(f, md))[:300], '\n')
print ()
if has_firefox_ext(md, extensions):
return True
if md.hasUrls():
urls = [unicode(u.toString()) for u in
md.urls()]
purls = [urlparse(u) for u in urls]
if DEBUG:
prints('URLS:', urls)
prints('Paths:', [u2p(x) for x in purls])
exts = frozenset([posixpath.splitext(u.path)[1][1:].lower() for u in
purls])
return bool(exts.intersection(frozenset(extensions)))
return False
def u2p(url):
path = url.path
if iswindows:
if path.startswith('/'):
path = path[1:]
ans = path.replace('/', os.sep)
if os.path.exists(ans):
return ans
# Try unquoting the URL
return urllib.unquote(ans)
def dnd_get_image(md, image_exts=IMAGE_EXTENSIONS):
'''
Get the image in the QMimeData object md.
:return: None, None if no image is found
QPixmap, None if an image is found, the pixmap is guaranteed not
null
url, filename if a URL that points to an image is found
'''
if dnd_has_image(md):
for x in md.formats():
x = unicode(x)
if x.startswith('image/'):
cdata = bytes(md.data(x))
pmap = QPixmap()
pmap.loadFromData(cdata)
if not pmap.isNull():
return pmap, None
break
# No image, look for a URL pointing to an image
if md.hasUrls():
urls = [unicode(u.toString()) for u in
md.urls()]
purls = [urlparse(u) for u in urls]
# First look for a local file
images = [u2p(x) for x in purls if x.scheme in ('', 'file') and
posixpath.splitext(urllib.unquote(x.path))[1][1:].lower() in
image_exts]
images = [x for x in images if os.path.exists(x)]
p = QPixmap()
for path in images:
try:
with open(path, 'rb') as f:
p.loadFromData(f.read())
except:
continue
if not p.isNull():
return p, None
# No local images, look for remote ones
# First, see if this is from Firefox
rurl, fname = get_firefox_rurl(md, image_exts)
if rurl and fname:
return rurl, fname
# Look through all remaining URLs
remote_urls = [x for x in purls if x.scheme in ('http', 'https',
'ftp') and posixpath.splitext(x.path)[1][1:].lower() in image_exts]
if remote_urls:
rurl = remote_urls[0]
fname = posixpath.basename(urllib.unquote(rurl.path))
return urlunparse(rurl), fname
return None, None
def dnd_get_files(md, exts):
'''
Get the file in the QMimeData object md with an extension that is one of
the extensions in exts.
:return: None, None if no file is found
[paths], None if a local file is found
[urls], [filenames] if URLs that point to a files are found
'''
# Look for a URL pointing to a file
if md.hasUrls():
urls = [unicode(u.toString()) for u in
md.urls()]
purls = [urlparse(u) for u in urls]
# First look for a local file
local_files = [u2p(x) for x in purls if x.scheme in ('', 'file') and
posixpath.splitext(urllib.unquote(x.path))[1][1:].lower() in
exts]
local_files = [x for x in local_files if os.path.exists(x)]
if local_files:
return local_files, None
# No local files, look for remote ones
# First, see if this is from Firefox
rurl, fname = get_firefox_rurl(md, exts)
if rurl and fname:
return [rurl], [fname]
# Look through all remaining URLs
remote_urls = [x for x in purls if x.scheme in ('http', 'https',
'ftp') and posixpath.splitext(x.path)[1][1:].lower() in exts]
if remote_urls:
filenames = [posixpath.basename(urllib.unquote(rurl.path)) for rurl in
remote_urls]
return [urlunparse(x) for x in remote_urls], filenames
return None, None
def _get_firefox_pair(md, exts, url, fname):
url = bytes(md.data(url)).decode('utf-16')
fname = bytes(md.data(fname)).decode('utf-16')
while url.endswith('\x00'):
url = url[:-1]
while fname.endswith('\x00'):
fname = fname[:-1]
if not url or not fname:
return None, None
ext = posixpath.splitext(fname)[1][1:].lower()
# Weird firefox bug on linux
ext = {'jpe':'jpg', 'epu':'epub', 'mob':'mobi'}.get(ext, ext)
fname = os.path.splitext(fname)[0] + '.' + ext
if DEBUG:
prints('Firefox file promise:', url, fname)
if ext not in exts:
fname = url = None
return url, fname
def get_firefox_rurl(md, exts):
formats = frozenset([unicode(x) for x in md.formats()])
url = fname = None
if 'application/x-moz-file-promise-url' in formats and \
'application/x-moz-file-promise-dest-filename' in formats:
try:
url, fname = _get_firefox_pair(md, exts,
'application/x-moz-file-promise-url',
'application/x-moz-file-promise-dest-filename')
except:
if DEBUG:
import traceback
traceback.print_exc()
if url is None and 'text/x-moz-url-data' in formats and \
'text/x-moz-url-desc' in formats:
try:
url, fname = _get_firefox_pair(md, exts,
'text/x-moz-url-data', 'text/x-moz-url-desc')
except:
if DEBUG:
import traceback
traceback.print_exc()
if url is None and '_NETSCAPE_URL' in formats:
try:
raw = bytes(md.data('_NETSCAPE_URL'))
raw = raw.decode('utf-8')
lines = raw.splitlines()
if len(lines) > 1 and re.match(r'[a-z]+://', lines[1]) is None:
url, fname = lines[:2]
ext = posixpath.splitext(fname)[1][1:].lower()
if ext not in exts:
fname = url = None
except:
if DEBUG:
import traceback
traceback.print_exc()
if DEBUG:
prints('Firefox rurl:', url, fname)
return url, fname
def has_firefox_ext(md, exts):
return bool(get_firefox_rurl(md, exts)[0])

View File

@ -264,6 +264,9 @@ class LayoutMixin(object): # {{{
self.book_details.files_dropped.connect(self.iactions['Add Books'].files_dropped_on_book)
self.book_details.cover_changed.connect(self.bd_cover_changed,
type=Qt.QueuedConnection)
self.book_details.remote_file_dropped.connect(
self.iactions['Add Books'].remote_file_dropped_on_book,
type=Qt.QueuedConnection)
self.book_details.open_containing_folder.connect(self.iactions['View'].view_folder_for_id)
self.book_details.view_specific_format.connect(self.iactions['View'].view_format_by_id)

View File

@ -3,7 +3,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
Miscellaneous widgets used in the GUI
'''
import re, os, traceback
import re, traceback
from PyQt4.Qt import QIcon, QFont, QLabel, QListWidget, QAction, \
QListWidgetItem, QTextCharFormat, QApplication, \
@ -22,6 +22,8 @@ from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata.meta import metadata_from_filename
from calibre.utils.config import prefs, XMLConfig, tweaks
from calibre.gui2.progress_indicator import ProgressIndicator as _ProgressIndicator
from calibre.gui2.dnd import dnd_has_image, dnd_get_image, dnd_get_files, \
IMAGE_EXTENSIONS, dnd_has_extension, DownloadDialog
history = XMLConfig('history')
@ -141,36 +143,35 @@ class FilenamePattern(QWidget, Ui_Form):
return pat
IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'gif', 'png', 'bmp']
class FormatList(QListWidget):
DROPABBLE_EXTENSIONS = BOOK_EXTENSIONS
formats_dropped = pyqtSignal(object, object)
delete_format = pyqtSignal()
@classmethod
def paths_from_event(cls, event):
'''
Accept a drop event and return a list of paths that can be read from
and represent files with extensions.
'''
if event.mimeData().hasFormat('text/uri-list'):
urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
urls = [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
return [u for u in urls if os.path.splitext(u)[1][1:].lower() in cls.DROPABBLE_EXTENSIONS]
def dragEnterEvent(self, event):
if int(event.possibleActions() & Qt.CopyAction) + \
int(event.possibleActions() & Qt.MoveAction) == 0:
return
paths = self.paths_from_event(event)
if paths:
md = event.mimeData()
if dnd_has_extension(md, self.DROPABBLE_EXTENSIONS):
event.acceptProposedAction()
def dropEvent(self, event):
paths = self.paths_from_event(event)
event.setDropAction(Qt.CopyAction)
self.formats_dropped.emit(event, paths)
md = event.mimeData()
# Now look for ebook files
urls, filenames = dnd_get_files(md, self.DROPABBLE_EXTENSIONS)
if not urls:
# Nothing found
return
if not filenames:
# Local files
self.formats_dropped.emit(event, urls)
else:
# Remote files, use the first file
d = DownloadDialog(urls[0], filenames[0], self)
d.start_download()
if d.err is None:
self.formats_dropped.emit(event, [d.fpath])
def dragMoveEvent(self, event):
event.acceptProposedAction()
@ -183,7 +184,7 @@ class FormatList(QListWidget):
class ImageDropMixin(object): # {{{
'''
Adds support for dropping images onto widgets and a contect menu for
Adds support for dropping images onto widgets and a context menu for
copy/pasting images.
'''
DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS
@ -191,39 +192,36 @@ class ImageDropMixin(object): # {{{
def __init__(self):
self.setAcceptDrops(True)
@classmethod
def paths_from_event(cls, event):
'''
Accept a drop event and return a list of paths that can be read from
and represent files with extensions.
'''
if event.mimeData().hasFormat('text/uri-list'):
urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
urls = [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
return [u for u in urls if os.path.splitext(u)[1][1:].lower() in cls.DROPABBLE_EXTENSIONS]
def dragEnterEvent(self, event):
if int(event.possibleActions() & Qt.CopyAction) + \
int(event.possibleActions() & Qt.MoveAction) == 0:
return
paths = self.paths_from_event(event)
if paths:
md = event.mimeData()
if dnd_has_extension(md, self.DROPABBLE_EXTENSIONS) or \
dnd_has_image(md):
event.acceptProposedAction()
def dropEvent(self, event):
paths = self.paths_from_event(event)
event.setDropAction(Qt.CopyAction)
for path in paths:
pmap = QPixmap()
pmap.load(path)
if not pmap.isNull():
self.handle_image_drop(path, pmap)
event.accept()
break
md = event.mimeData()
def handle_image_drop(self, path, pmap):
x, y = dnd_get_image(md)
if x is not None:
# We have an image, set cover
event.accept()
if y is None:
# Local image
self.handle_image_drop(x)
else:
# Remote files, use the first file
d = DownloadDialog(x, y, self)
d.start_download()
if d.err is None:
pmap = QPixmap()
pmap.loadFromData(open(d.fpath, 'rb').read())
if not pmap.isNull():
self.handle_image_drop(pmap)
def handle_image_drop(self, pmap):
self.set_pixmap(pmap)
self.cover_changed.emit(open(path, 'rb').read())
self.cover_changed.emit(pixmap_to_data(pmap))
def dragMoveEvent(self, event):
event.acceptProposedAction()

View File

@ -12,13 +12,13 @@ from calibre.constants import DEBUG
from calibre.utils.config import Config, StringConfig, tweaks
from calibre.utils.formatter import TemplateFormatter
from calibre.utils.filenames import shorten_components_to, supports_long_names, \
ascii_filename, sanitize_file_name
ascii_filename
from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ebooks.metadata.meta import set_metadata
from calibre.constants import preferred_encoding, filesystem_encoding
from calibre.constants import preferred_encoding
from calibre.ebooks.metadata import fmt_sidx
from calibre.ebooks.metadata import title_sort
from calibre import strftime, prints
from calibre import strftime, prints, sanitize_file_name_unicode
plugboard_any_device_value = 'any device'
plugboard_any_format_value = 'any format'
@ -197,12 +197,10 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
format_args[key] = ''
components = SafeFormat().safe_format(template, format_args,
'G_C-EXCEPTION!', mi)
components = [x.strip() for x in components.split('/') if x.strip()]
components = [x.strip() for x in components.split('/')]
components = [sanitize_func(x) for x in components if x]
if not components:
components = [str(id)]
components = [x.encode(filesystem_encoding, 'replace') if isinstance(x,
unicode) else x for x in components]
if to_lowercase:
components = [x.lower() for x in components]
if replace_whitespace:
@ -247,7 +245,7 @@ def do_save_book_to_disk(id_, mi, cover, plugboards,
return True, id_, mi.title
components = get_components(opts.template, mi, id_, opts.timefmt, length,
ascii_filename if opts.asciiize else sanitize_file_name,
ascii_filename if opts.asciiize else sanitize_file_name_unicode,
to_lowercase=opts.to_lowercase,
replace_whitespace=opts.replace_whitespace)
base_path = os.path.join(root, *components)
@ -329,8 +327,6 @@ def do_save_book_to_disk(id_, mi, cover, plugboards,
def _sanitize_args(root, opts):
if opts is None:
opts = config().parse()
if isinstance(root, unicode):
root = root.encode(filesystem_encoding)
root = os.path.abspath(root)
opts.template = preprocess_template(opts.template)

View File

@ -72,47 +72,6 @@ if not _run_once:
pass
################################################################################
# Improve builtin path functions to handle unicode sensibly
_abspath = os.path.abspath
def my_abspath(path, encoding=sys.getfilesystemencoding()):
'''
Work around for buggy os.path.abspath. This function accepts either byte strings,
in which it calls os.path.abspath, or unicode string, in which case it first converts
to byte strings using `encoding`, calls abspath and then decodes back to unicode.
'''
to_unicode = False
if encoding is None:
encoding = preferred_encoding
if isinstance(path, unicode):
path = path.encode(encoding)
to_unicode = True
res = _abspath(path)
if to_unicode:
res = res.decode(encoding)
return res
os.path.abspath = my_abspath
_join = os.path.join
def my_join(a, *p):
encoding=sys.getfilesystemencoding()
if not encoding:
encoding = preferred_encoding
p = [a] + list(p)
_unicode = False
for i in p:
if isinstance(i, unicode):
_unicode = True
break
p = [i.encode(encoding) if isinstance(i, unicode) else i for i in p]
res = _join(*p)
if _unicode:
res = res.decode(encoding)
return res
os.path.join = my_join
def local_open(name, mode='r', bufsize=-1):
'''

View File

@ -19,7 +19,7 @@ in the working tree you want to use it with::
trac_reponame_password = <password>
'''
import os, re, xmlrpclib
import os, re, xmlrpclib, subprocess
from bzrlib.builtins import cmd_commit as _cmd_commit, tree_files
from bzrlib import branch
import bzrlib
@ -115,5 +115,7 @@ class cmd_commit(_cmd_commit):
server.ticket.update(int(bug), msg,
{'status':'closed', 'resolution':'fixed'},
True)
subprocess.Popen('/home/kovid/work/kde/mail.py -f --delay 10'.split())
bzrlib.commands.register_command(cmd_commit)