Implement adding files to the book

This commit is contained in:
Kovid Goyal 2013-11-17 12:11:38 +05:30
parent 1f0f3038c1
commit b1652a2316
10 changed files with 868 additions and 15 deletions

602
imgsrc/document-new.svg Normal file
View File

@ -0,0 +1,602 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
<svg
xmlns:ns="http://ns.adobe.com/SaveForWeb/1.0/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
width="128"
height="128"
viewBox="0 0 128 128"
overflow="visible"
enable-background="new 0 0 128 128"
xml:space="preserve"
sodipodi:version="0.32"
inkscape:version="0.45+devel"
sodipodi:docname="document-new.svgz"
sodipodi:docbase="/Users/david/Desktop/sandbox"
inkscape:output_extension="org.inkscape.output.svgz.inkscape"
inkscape:export-filename="/Users/david/Desktop/sandbox/document-new2.png"
inkscape:export-xdpi="22.5"
inkscape:export-ydpi="22.5"><defs
id="defs105"><filter
inkscape:collect="always"
id="filter5943"><feGaussianBlur
inkscape:collect="always"
stdDeviation="1.04"
id="feGaussianBlur5945" /></filter><linearGradient
id="linearGradient6740"><stop
style="stop-color:#004d00;stop-opacity:0;"
offset="0"
id="stop6742" /><stop
id="stop6748"
offset="0.5"
style="stop-color:#004d00;stop-opacity:1;" /><stop
style="stop-color:#004d00;stop-opacity:0;"
offset="1"
id="stop6744" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient6740"
id="linearGradient6930"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(20.000035,-56.000003)"
x1="102"
y1="118"
x2="102"
y2="65.932846" /><linearGradient
gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)"
y2="-383.9975"
x2="-23.516129"
y1="-383.9971"
x1="-84.002403"
gradientUnits="userSpaceOnUse"
id="linearGradient3711"><stop
id="stop3713"
style="stop-color:white;stop-opacity:1;"
offset="0" /><stop
id="stop3715"
style="stop-color:white;stop-opacity:0;"
offset="1" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3711"
id="linearGradient8927"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0,0.7388168,-0.7388168,0,-1.5226652,63.255682)"
x1="-80.00296"
y1="-131.93112"
x2="-45.096584"
y2="-131.93112" /><filter
inkscape:collect="always"
id="filter7317"><feGaussianBlur
inkscape:collect="always"
stdDeviation="2.8805897"
id="feGaussianBlur7319" /></filter><clipPath
clipPathUnits="userSpaceOnUse"
id="clipPath5163"><circle
sodipodi:ry="36"
sodipodi:rx="36"
sodipodi:cy="92"
sodipodi:cx="343.99899"
style="fill:url(#linearGradient5167);fill-opacity:1"
r="36"
rx="8.0010004"
cx="343.99899"
cy="92"
ry="8.0010004"
id="circle5165" /></clipPath><linearGradient
id="linearGradient4296"><stop
id="stop4298"
offset="0"
style="stop-color:#00ff00;stop-opacity:1" /><stop
id="stop4300"
offset="1"
style="stop-color:#006500;stop-opacity:1" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient4296"
id="linearGradient4272"
gradientUnits="userSpaceOnUse"
x1="328.12448"
y1="120.81158"
x2="336.98077"
y2="87.759453" /><linearGradient
gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)"
y2="-383.9971"
x2="-12.0029"
y1="-383.9971"
x1="-84.002403"
gradientUnits="userSpaceOnUse"
id="linearGradient4770"><stop
id="stop4772"
style="stop-color:#e5ff00;stop-opacity:1"
offset="0" /><stop
id="stop4774"
style="stop-color:#bff500;stop-opacity:0;"
offset="1" /></linearGradient><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient4770"
id="radialGradient8920"
gradientUnits="userSpaceOnUse"
cx="343.99899"
cy="92"
fx="343.99899"
fy="92"
r="36" /><linearGradient
gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)"
y2="-383.9971"
x2="-12.0029"
y1="-383.9971"
x1="-84.002403"
gradientUnits="userSpaceOnUse"
id="linearGradient5958"><stop
id="stop5960"
style="stop-color:#008c00;stop-opacity:1"
offset="0" /><stop
id="stop5962"
style="stop-color:#00bf00;stop-opacity:1"
offset="1" /></linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5958"
id="linearGradient8916"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)"
x1="-86.120354"
y1="-381.09921"
x2="-56.357521"
y2="-373.1243" /><filter
inkscape:collect="always"
id="filter4292"><feGaussianBlur
inkscape:collect="always"
stdDeviation="2.1604423"
id="feGaussianBlur4294" /></filter><linearGradient
y2="0"
x2="28"
y1="57.5"
x1="28"
gradientUnits="userSpaceOnUse"
id="linearGradient18668">
<stop
id="stop18670"
style="stop-color:#fffccf;stop-opacity:1;"
offset="0" />
<stop
id="stop18672"
style="stop-color:white;stop-opacity:0;"
offset="1" />
</linearGradient><linearGradient
id="XMLID_2_"
gradientUnits="userSpaceOnUse"
x1="28"
y1="57.5"
x2="28"
y2="0">
<stop
offset="0"
style="stop-color:#FFEA00"
id="stop12" />
<stop
offset="1"
style="stop-color:#FFCC00"
id="stop14" />
</linearGradient><linearGradient
inkscape:collect="always"
xlink:href="#XMLID_9_"
id="linearGradient2291"
gradientUnits="userSpaceOnUse"
x1="94.3438"
y1="102.3447"
x2="86.5356"
y2="94.5366" /><linearGradient
inkscape:collect="always"
xlink:href="#XMLID_10_"
id="linearGradient2293"
gradientUnits="userSpaceOnUse"
x1="95"
y1="103"
x2="86.5865"
y2="94.5865" /><linearGradient
inkscape:collect="always"
xlink:href="#XMLID_11_"
id="linearGradient2295"
gradientUnits="userSpaceOnUse"
x1="95"
y1="103"
x2="87.293"
y2="95.293" /><linearGradient
inkscape:collect="always"
xlink:href="#XMLID_12_"
id="linearGradient2297"
gradientUnits="userSpaceOnUse"
x1="96"
y1="104"
x2="88.0002"
y2="96.0002" /><linearGradient
inkscape:collect="always"
xlink:href="#XMLID_2_"
id="linearGradient2299"
gradientUnits="userSpaceOnUse"
x1="28"
y1="57.5"
x2="28"
y2="0" /><radialGradient
inkscape:collect="always"
xlink:href="#XMLID_8_"
id="radialGradient2272"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.9787234,0,0,0.9818182,1.3617045,1.1636364)"
cx="102"
cy="112.3047"
r="139.55859" /><radialGradient
inkscape:collect="always"
xlink:href="#XMLID_7_"
id="radialGradient2275"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(2.4e-6,0)"
cx="102"
cy="112.3047"
r="139.5585" /><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient18668"
id="radialGradient3239"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,1.2916104,-4,-19.57938)"
cx="30.010139"
cy="34.750149"
fx="30.010139"
fy="34.750149"
r="20.78125" /><filter
inkscape:collect="always"
id="filter3241"><feGaussianBlur
inkscape:collect="always"
stdDeviation="1.0394514"
id="feGaussianBlur3243" /></filter></defs><sodipodi:namedview
inkscape:window-height="670"
inkscape:window-width="1022"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
guidetolerance="10.0"
gridtolerance="10.0"
objecttolerance="10.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base"
inkscape:zoom="2.8284271"
inkscape:cx="63.856972"
inkscape:cy="57.431611"
inkscape:window-x="2"
inkscape:window-y="27"
inkscape:current-layer="Layer_1"
showgrid="true"
gridspacingx="4px"
gridspacingy="4px"
gridempspacing="2"
showguides="true"
inkscape:guide-bbox="true" />
<metadata
id="metadata3">
<ns:sfw>
<ns:slices>
<ns:slice
y="0"
x="0"
height="128"
width="128"
sliceID="1316743234" />
</ns:slices>
<ns:sliceSourceBounds
y="0"
x="0"
height="128"
width="128"
bottomLeftOrigin="true" />
<ns:optimizationSettings>
<ns:targetSettings
targetSettingsID="0"
fileFormat="PNG24Format">
<ns:PNG24Format
transparency="true"
filtered="false"
matteColor="#FFFFFF"
noMatteColor="false"
interlaced="false">
</ns:PNG24Format>
</ns:targetSettings>
<ns:targetSettings
targetSettingsID="1696735251"
fileFormat="PNG24Format">
<ns:PNG24Format
transparency="true"
filtered="false"
matteColor="#FFFFFF"
noMatteColor="false"
interlaced="false">
</ns:PNG24Format>
</ns:targetSettings>
</ns:optimizationSettings>
</ns:sfw>
<rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata>
<radialGradient
id="XMLID_7_"
cx="102"
cy="112.3047"
r="139.5585"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
style="stop-color:#00537D"
id="stop16" />
<stop
offset="0.0151"
style="stop-color:#186389"
id="stop18" />
<stop
offset="0.0558"
style="stop-color:#558CA8"
id="stop20" />
<stop
offset="0.0964"
style="stop-color:#89AFC3"
id="stop22" />
<stop
offset="0.1357"
style="stop-color:#B3CCD8"
id="stop24" />
<stop
offset="0.1737"
style="stop-color:#D4E2E9"
id="stop26" />
<stop
offset="0.2099"
style="stop-color:#ECF2F5"
id="stop28" />
<stop
offset="0.2435"
style="stop-color:#FAFCFD"
id="stop30" />
<stop
offset="0.2722"
style="stop-color:#FFFFFF"
id="stop32" />
</radialGradient>
<radialGradient
id="XMLID_8_"
cx="102"
cy="112.3047"
r="139.55859"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
style="stop-color:#535557"
id="stop37" />
<stop
offset="0.11366145"
style="stop-color:#898A8C"
id="stop41" />
<stop
offset="0.20296688"
style="stop-color:#ECECEC"
id="stop47" />
<stop
offset="0.2363"
style="stop-color:#FAFAFA"
id="stop49" />
<stop
offset="0.2722"
style="stop-color:#FFFFFF"
id="stop51" />
<stop
offset="0.5313"
style="stop-color:#FAFAFA"
id="stop53" />
<stop
offset="0.8449"
style="stop-color:#EBECEC"
id="stop55" />
<stop
offset="1"
style="stop-color:#E1E2E3"
id="stop57" />
</radialGradient>
<linearGradient
id="XMLID_9_"
gradientUnits="userSpaceOnUse"
x1="94.3438"
y1="102.3447"
x2="86.5356"
y2="94.5366">
<stop
offset="0"
style="stop-color:#FFFFFF"
id="stop62" />
<stop
offset="1"
style="stop-color:#555753"
id="stop64" />
</linearGradient>
<linearGradient
id="XMLID_10_"
gradientUnits="userSpaceOnUse"
x1="95"
y1="103"
x2="86.5865"
y2="94.5865">
<stop
offset="0"
style="stop-color:#FFFFFF"
id="stop69" />
<stop
offset="1"
style="stop-color:#555753"
id="stop71" />
</linearGradient>
<linearGradient
id="XMLID_11_"
gradientUnits="userSpaceOnUse"
x1="95"
y1="103"
x2="87.293"
y2="95.293">
<stop
offset="0"
style="stop-color:#FFFFFF"
id="stop76" />
<stop
offset="1"
style="stop-color:#393B38"
id="stop78" />
</linearGradient>
<linearGradient
id="XMLID_12_"
gradientUnits="userSpaceOnUse"
x1="96"
y1="104"
x2="88.0002"
y2="96.0002">
<stop
offset="0"
style="stop-color:#888A85"
id="stop83" />
<stop
offset="0.0072"
style="stop-color:#8C8E89"
id="stop85" />
<stop
offset="0.0673"
style="stop-color:#ABACA9"
id="stop87" />
<stop
offset="0.1347"
style="stop-color:#C5C6C4"
id="stop89" />
<stop
offset="0.2115"
style="stop-color:#DBDBDA"
id="stop91" />
<stop
offset="0.3012"
style="stop-color:#EBEBEB"
id="stop93" />
<stop
offset="0.4122"
style="stop-color:#F7F7F6"
id="stop95" />
<stop
offset="0.5679"
style="stop-color:#FDFDFD"
id="stop97" />
<stop
offset="1"
style="stop-color:#FFFFFF"
id="stop99" />
</linearGradient>
<path
id="path6594"
d="M 23,9 L 23.040816,121 L 84.172,121 C 84.702,121 85.211,120.789 85.586,120.414 L 118.414,87.586 C 118.789,87.211 119,86.702 119,86.172 L 119,9 L 23,9 z "
style="opacity:0.5;fill:#000000;fill-opacity:1;filter:url(#filter3241)"
transform="matrix(1.0416667,0,0,1.0357143,-9.9583333,-3.3215342)"
sodipodi:nodetypes="cccccccc" /><path
style="fill:url(#radialGradient2275)"
d="M 16.000002,8 L 16.000002,120 L 77.172002,120 C 77.702002,120 78.211002,119.789 78.586002,119.414 L 111.414,86.586 C 111.789,86.211 112,85.702 112,85.172 L 112,8 L 16.000002,8 z "
id="path34"
sodipodi:nodetypes="cccccccc" /><path
style="fill:url(#radialGradient2272);fill-opacity:1"
d="M 18.978725,10 C 18.439449,10 18.000002,10.440836 18.000002,10.981818 L 18.000002,117.01818 C 18.000002,117.56015 18.439449,118 18.978725,118 L 76.891747,118 C 77.149151,118 77.401662,117.89593 77.583704,117.71233 L 109.71323,85.4812 C 109.89626,85.2976 110,85.045273 110,84.787055 L 110,10.981818 C 110,10.440836 109.56153,10 109.02128,10 L 18.978725,10 z "
id="path59" /><g
id="g1973"
transform="translate(2.4e-6,0)"><path
style="opacity:0.1;fill:url(#linearGradient2291)"
id="path66"
d="M 111.414,86.586 C 111.664,86.336 93.035,93 88,93 C 86.346,93 85,94.346 85,96 C 85,101.035 78.336,119.664 78.586,119.414 L 111.414,86.586 z " /><path
style="opacity:0.1;fill:url(#linearGradient2293)"
id="path73"
d="M 111.414,86.586 C 111.789,86.211 97.444,94 88,94 C 86.897,94 86,94.897 86,96 C 86,105.444 78.211,119.789 78.586,119.414 L 111.414,86.586 z " /><path
style="opacity:0.1;fill:url(#linearGradient2295)"
id="path80"
d="M 111.414,86.586 C 111.653,86.347 97.807,95 88,95 C 87.447,95 87,95.447 87,96 C 87,105.807 78.347,119.653 78.586,119.414 L 111.414,86.586 z " /><path
style="fill:url(#linearGradient2297)"
id="path101"
d="M 78.586,119.414 C 78.586,119.414 90.5,109.5 96,104 C 101.5,98.5 111.414,86.586 111.414,86.586 C 111.414,86.586 98.25,96 88,96 C 88,106.25 78.586,119.414 78.586,119.414 z " /></g>
<g
id="g3640"
transform="translate(9.545815e-6,1.710865e-5)"><circle
sodipodi:ry="36"
sodipodi:rx="36"
sodipodi:cy="92"
sodipodi:cx="343.99899"
style="opacity:0.5;fill:#000000;fill-opacity:1;filter:url(#filter4292)"
r="36"
rx="8.0010004"
cx="343.99899"
cy="92"
ry="8.0010004"
id="circle4274"
transform="matrix(-0.858425,0.2300143,-0.2300143,-0.858425,412.45864,35.85043)" /><circle
transform="matrix(-0.858425,0.2300143,-0.2300143,-0.858425,412.45864,31.85043)"
id="circle3581_2_"
ry="8.0010004"
cy="92"
cx="343.99899"
rx="8.0010004"
r="36"
style="fill:url(#linearGradient8916);fill-opacity:1"
sodipodi:cx="343.99899"
sodipodi:cy="92"
sodipodi:rx="36"
sodipodi:ry="36" /><circle
sodipodi:ry="36"
sodipodi:rx="36"
sodipodi:cy="92"
sodipodi:cx="343.99899"
style="fill:url(#radialGradient8920);fill-opacity:1"
r="36"
rx="8.0010004"
cx="343.99899"
cy="92"
ry="8.0010004"
id="circle4383"
transform="matrix(-0.6438188,0.1725107,-0.1725107,-0.6438188,333.34401,31.887831)" /><circle
sodipodi:ry="36"
sodipodi:rx="36"
sodipodi:cy="92"
sodipodi:cx="343.99899"
style="opacity:0.8;fill:none;fill-opacity:1;stroke:url(#linearGradient4272);stroke-width:6.75138187;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter7317)"
r="36"
rx="8.0010004"
cx="343.99899"
cy="92"
ry="8.0010004"
id="circle4776"
transform="matrix(-0.858425,-0.2300143,-0.2300143,0.858425,412.45864,32.149572)"
clip-path="url(#clipPath5163)" /><path
id="circle16776"
d="M 96.000027,4.1481901 C 84.654311,4.1481901 75.173932,12.159796 72.888913,22.826405 C 77.443574,27.0723 86.085806,29.937514 96.000027,29.937514 C 105.91426,29.937514 114.55648,27.0723 119.11114,22.826405 C 116.82613,12.159796 107.34574,4.1481901 96.000027,4.1481901 z"
style="opacity:0.8;fill:url(#linearGradient8927);fill-opacity:1" /><g
transform="translate(-26.000031,-3.999996)"
id="g6850"><path
id="path4123"
d="M 118.00003,15.999997 L 118.00003,31.999997 L 102.00003,31.999997 L 102.00003,39.999997 L 118.00003,39.999997 L 118.00003,55.999997 L 126.00003,55.999997 L 126.00003,39.999997 L 142.00003,39.999997 L 142.00003,31.999997 L 126.00003,31.999997 L 126.00003,15.999997 L 118.00003,15.999997 z"
style="fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient6930);stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;filter:url(#filter5943)"
sodipodi:nodetypes="ccccccccccccc" /><path
style="fill:#ffffff;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:4;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1"
d="M 118.00003,15.999997 L 118.00003,31.999997 L 102.00003,31.999997 L 102.00003,39.999997 L 118.00003,39.999997 L 118.00003,55.999997 L 126.00003,55.999997 L 126.00003,39.999997 L 142.00003,39.999997 L 142.00003,31.999997 L 126.00003,31.999997 L 126.00003,15.999997 L 118.00003,15.999997 z"
id="rect3232"
sodipodi:nodetypes="ccccccccccccc" /></g></g></svg>

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

@ -158,6 +158,46 @@ class Container(object): # {{{
for name, path in self.name_path_map.iteritems()} for name, path in self.name_path_map.iteritems()}
} }
def add_file(self, name, data, media_type=None):
''' Add a file to this container. Entries for the file are
automatically created in the OPF manifest and spine
(if the file is a text document) '''
if self.has_name(name):
raise ValueError('A file with the name %s already exists' % name)
if '..' in name:
raise ValueError('Names are not allowed to have .. in them')
href = self.name_to_href(name, self.opf_name)
all_hrefs = {x.get('href') for x in self.opf_xpath('//opf:manifest/opf:item[@href]')}
if href in all_hrefs:
raise ValueError('An item with the href %s already exists in the manifest' % href)
path = self.name_to_abspath(name)
base = os.path.dirname(path)
if not os.path.exists(base):
os.makedirs(base)
with open(path, 'wb') as f:
f.write(data)
mt = media_type or guess_type(name)
self.name_path_map[name] = path
self.mime_map[name] = mt
if name in self.names_that_need_not_be_manifested:
return
all_ids = {x.get('id') for x in self.opf_xpath('//*[@id]')}
c = 0
item_id = 'id'
while item_id in all_ids:
c += 1
item_id = 'id' + '%d'%c
manifest = self.opf_xpath('//opf:manifest')[0]
item = manifest.makeelement(OPF('item'),
id=item_id, href=href)
item.set('media-type', mt)
self.insert_into_xml(manifest, item)
self.dirty(self.opf_name)
if mt in OEB_DOCS:
spine = self.opf_xpath('//opf:spine')[0]
si = manifest.makeelement(OPF('itemref'), idref=item_id)
self.insert_into_xml(spine, si)
def rename(self, current_name, new_name): def rename(self, current_name, new_name):
''' Renames a file from current_name to new_name. It automatically ''' Renames a file from current_name to new_name. It automatically
rebases all links inside the file if the directory the file is in rebases all links inside the file if the directory the file is in

View File

@ -158,3 +158,20 @@ class ContainerTests(BaseTest):
# self.run_external_tools(c, gvim=True) # self.run_external_tools(c, gvim=True)
def test_file_add(self):
' Test adding of files '
book = get_simple_book()
c = get_container(book)
name = 'folder/added file.html'
c.add_file(name, b'xxx')
self.assertEqual('xxx', c.raw_data(name))
self.assertIn(name, set(c.manifest_id_map.itervalues()))
self.assertIn(name, {x[0] for x in c.spine_names})
name = 'added.css'
c.add_file(name, b'xxx')
self.assertEqual('xxx', c.raw_data(name))
self.assertIn(name, set(c.manifest_id_map.itervalues()))
self.assertNotIn(name, {x[0] for x in c.spine_names})
self.check_links(c)

View File

@ -24,6 +24,7 @@ from calibre.gui2 import error_dialog, choose_files, question_dialog, info_dialo
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.tweak_book import set_current_container, current_container, tprefs, actions, editors from calibre.gui2.tweak_book import set_current_container, current_container, tprefs, actions, editors
from calibre.gui2.tweak_book.undo import GlobalUndoHistory from calibre.gui2.tweak_book.undo import GlobalUndoHistory
from calibre.gui2.tweak_book.file_list import NewFileDialog
from calibre.gui2.tweak_book.save import SaveManager from calibre.gui2.tweak_book.save import SaveManager
from calibre.gui2.tweak_book.preview import parse_worker from calibre.gui2.tweak_book.preview import parse_worker
from calibre.gui2.tweak_book.toc import TOCEditor from calibre.gui2.tweak_book.toc import TOCEditor
@ -144,15 +145,52 @@ class Boss(QObject):
if not editors: if not editors:
self.gui.preview.clear() self.gui.preview.clear()
def check_opf_dirtied(self):
c = current_container()
if c.opf_name in editors and editors[c.opf_name].is_modified:
return question_dialog(self.gui, _('Unsaved changes'), _(
'You have unsaved changes in %s. If you proceed,'
' you will lose them. Proceed anyway?') % c.opf_name)
return True
def reorder_spine(self, items): def reorder_spine(self, items):
# TODO: If content.opf is dirty in an editor, abort, calling if not self.check_opf_dirtied():
# file_list.build(current_container) to undo drag and drop return
self.add_savepoint(_('Re-order text')) self.add_savepoint(_('Re-order text'))
c = current_container() c = current_container()
c.set_spine(items) c.set_spine(items)
self.gui.action_save.setEnabled(True) self.gui.action_save.setEnabled(True)
self.gui.file_list.build(current_container()) # needed as the linear flag may have changed on some items self.gui.file_list.build(current_container()) # needed as the linear flag may have changed on some items
# TODO: If content.opf is open in an editor, reload it if c.opf_name in editors:
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
def add_file(self):
if not self.check_opf_dirtied():
return
d = NewFileDialog(self.gui)
if d.exec_() != d.Accepted:
return
self.add_savepoint(_('Add file %s') % self.gui.elided_text(d.file_name))
c = current_container()
data = d.file_data
if d.using_template:
data = data.replace(b'%CURSOR%', b'')
try:
c.add_file(d.file_name, data)
except:
self.rewind_savepoint()
raise
self.gui.file_list.build(c)
self.gui.file_list.select_name(d.file_name)
if c.opf_name in editors:
editors[c.opf_name].replace_data(c.raw_data(c.opf_name))
mt = c.mime_map[d.file_name]
syntax = syntax_from_mime(mt)
if syntax:
if d.using_template:
self.edit_file(d.file_name, syntax, use_template=d.file_data.decode('utf-8'))
else:
self.edit_file(d.file_name, syntax)
def edit_toc(self): def edit_toc(self):
if not self.check_dirtied(): if not self.check_dirtied():
@ -454,21 +492,27 @@ class Boss(QObject):
_('Saving of the book failed. Click "Show Details"' _('Saving of the book failed. Click "Show Details"'
' for more information.'), det_msg=tb, show=True) ' for more information.'), det_msg=tb, show=True)
def init_editor(self, name, editor, data=None): def init_editor(self, name, editor, data=None, use_template=False):
editor.undo_redo_state_changed.connect(self.editor_undo_redo_state_changed) editor.undo_redo_state_changed.connect(self.editor_undo_redo_state_changed)
editor.data_changed.connect(self.editor_data_changed) editor.data_changed.connect(self.editor_data_changed)
editor.copy_available_state_changed.connect(self.editor_copy_available_state_changed) editor.copy_available_state_changed.connect(self.editor_copy_available_state_changed)
if data is not None: if data is not None:
if use_template:
editor.init_from_template(data)
else:
editor.data = data editor.data = data
editor.modification_state_changed.connect(self.editor_modification_state_changed) editor.modification_state_changed.connect(self.editor_modification_state_changed)
self.gui.central.add_editor(name, editor) self.gui.central.add_editor(name, editor)
def edit_file(self, name, syntax): def edit_file(self, name, syntax, use_template=None):
editor = editors.get(name, None) editor = editors.get(name, None)
if editor is None: if editor is None:
editor = editors[name] = editor_from_syntax(syntax, self.gui.editor_tabs) editor = editors[name] = editor_from_syntax(syntax, self.gui.editor_tabs)
if use_template is None:
data = current_container().raw_data(name) data = current_container().raw_data(name)
self.init_editor(name, editor, data) else:
data = use_template
self.init_editor(name, editor, data, use_template=bool(use_template))
self.show_editor(name) self.show_editor(name)
def show_editor(self, name): def show_editor(self, name):

View File

@ -115,11 +115,14 @@ class TextEdit(QPlainTextEdit):
self.highlight_color = theme_color(theme, 'HighlightRegion', 'bg') self.highlight_color = theme_color(theme, 'HighlightRegion', 'bg')
# }}} # }}}
def load_text(self, text, syntax='html'): def load_text(self, text, syntax='html', process_template=False):
self.highlighter = {'html':HTMLHighlighter, 'css':CSSHighlighter, 'xml':XMLHighlighter}.get(syntax, SyntaxHighlighter)(self) self.highlighter = {'html':HTMLHighlighter, 'css':CSSHighlighter, 'xml':XMLHighlighter}.get(syntax, SyntaxHighlighter)(self)
self.highlighter.apply_theme(self.theme) self.highlighter.apply_theme(self.theme)
self.highlighter.setDocument(self.document()) self.highlighter.setDocument(self.document())
self.setPlainText(text) self.setPlainText(text)
if process_template and QPlainTextEdit.find(self, '%CURSOR%'):
c = self.textCursor()
c.insertText('')
def replace_text(self, text): def replace_text(self, text):
c = self.textCursor() c = self.textCursor()

View File

@ -48,6 +48,9 @@ class Editor(QMainWindow):
self.editor.load_text(val, syntax=self.syntax) self.editor.load_text(val, syntax=self.syntax)
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def init_from_template(self, template):
self.editor.load_text(template, syntax=self.syntax, process_template=True)
def get_raw_data(self): def get_raw_data(self):
return unicode(self.editor.toPlainText()) return unicode(self.editor.toPlainText())

View File

@ -6,19 +6,22 @@ from __future__ import (unicode_literals, division, absolute_import,
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
import os
from binascii import hexlify from binascii import hexlify
from collections import OrderedDict from collections import OrderedDict
from PyQt4.Qt import ( from PyQt4.Qt import (
QWidget, QTreeWidget, QGridLayout, QSize, Qt, QTreeWidgetItem, QIcon, QWidget, QTreeWidget, QGridLayout, QSize, Qt, QTreeWidgetItem, QIcon,
QStyledItemDelegate, QStyle, QPixmap, QPainter, pyqtSignal) QStyledItemDelegate, QStyle, QPixmap, QPainter, pyqtSignal,
QDialogButtonBox, QDialog, QLabel, QLineEdit, QVBoxLayout)
from calibre import human_readable from calibre import human_readable, sanitize_file_name_unicode
from calibre.ebooks.oeb.base import OEB_STYLES, OEB_DOCS from calibre.ebooks.oeb.base import OEB_STYLES, OEB_DOCS
from calibre.ebooks.oeb.polish.container import guess_type from calibre.ebooks.oeb.polish.container import guess_type
from calibre.ebooks.oeb.polish.cover import get_cover_page_name, get_raster_cover_name from calibre.ebooks.oeb.polish.cover import get_cover_page_name, get_raster_cover_name
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog, choose_files
from calibre.gui2.tweak_book import current_container from calibre.gui2.tweak_book import current_container
from calibre.gui2.tweak_book.editor import syntax_from_mime from calibre.gui2.tweak_book.editor import syntax_from_mime
from calibre.gui2.tweak_book.templates import template_for
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
TOP_ICON_SIZE = 24 TOP_ICON_SIZE = 24
@ -128,6 +131,14 @@ class FileList(QTreeWidget):
if name in state['selected']: if name in state['selected']:
c.setSelected(True) c.setSelected(True)
def select_name(self, name):
for parent in self.categories.itervalues():
for c in (parent.child(i) for i in xrange(parent.childCount())):
q = unicode(c.data(0, NAME_ROLE).toString())
c.setSelected(q == name)
if q == name:
self.scrollToItem(c)
def build(self, container, preserve_state=True): def build(self, container, preserve_state=True):
if preserve_state: if preserve_state:
state = self.get_state() state = self.get_state()
@ -360,6 +371,86 @@ class FileList(QTreeWidget):
ans['selected'][name] = syntax_from_mime(mime) ans['selected'][name] = syntax_from_mime(mime)
return ans return ans
class NewFileDialog(QDialog):
def __init__(self, initial_choice='html', parent=None):
QDialog.__init__(self, parent)
self.l = l = QVBoxLayout()
self.setLayout(l)
self.la = la = QLabel(_(
'Choose a name for the new file'))
self.setWindowTitle(_('Choose file'))
l.addWidget(la)
self.name = n = QLineEdit(self)
n.textChanged.connect(self.update_ok)
l.addWidget(n)
self.err_label = la = QLabel('')
la.setWordWrap(True)
l.addWidget(la)
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
l.addWidget(bb)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
self.imp_button = b = bb.addButton(_('Import resource file (image/font/etc.)'), bb.ActionRole)
b.setIcon(QIcon(I('view-image.png')))
b.clicked.connect(self.import_file)
self.ok_button = bb.button(bb.Ok)
self.file_data = ''
self.using_template = False
def show_error(self, msg):
self.err_label.setText('<p style="color:red">' + msg)
return False
def import_file(self):
path = choose_files(self, 'tweak-book-new-resource-file', _('Choose file'), select_only_single_file=True)
if path:
path = path[0]
with open(path, 'rb') as f:
self.file_data = f.read()
name = os.path.basename(path)
self.name.setText(name)
@property
def name_is_ok(self):
name = unicode(self.name.text())
if not name or not name.strip():
return self.show_error('')
ext = name.rpartition('.')[-1]
if not ext or ext == name:
return self.show_error(_('The file name must have an extension'))
norm = name.replace('\\', '/')
parts = name.split('/')
for x in parts:
if sanitize_file_name_unicode(x) != x:
return self.show_error(_('The file name contains invalid characters'))
if current_container().has_name(norm):
return self.show_error(_('This file name already exists in the book'))
self.show_error('')
return True
def update_ok(self, *args):
self.ok_button.setEnabled(self.name_is_ok)
def accept(self):
if not self.name_is_ok:
return error_dialog(self, _('No name specified'), _(
'You must specify a name for the new file, with an extension, for example, chapter1.html'), show=True)
name = unicode(self.name.text())
name, ext = name.rpartition('.')[0::2]
name = (name + '.' + ext.lower()).replace('\\', '/')
mt = guess_type(name)
if mt in OEB_DOCS:
self.file_data = template_for('html').encode('utf-8')
self.using_template = True
elif mt in OEB_STYLES:
self.file_data = template_for('css').encode('utf-8')
self.using_template = True
self.file_name = name
QDialog.accept(self)
class FileListWidget(QWidget): class FileListWidget(QWidget):
delete_requested = pyqtSignal(object, object) delete_requested = pyqtSignal(object, object)
@ -375,7 +466,7 @@ class FileListWidget(QWidget):
self.layout().setContentsMargins(0, 0, 0, 0) self.layout().setContentsMargins(0, 0, 0, 0)
for x in ('delete_requested', 'reorder_spine', 'rename_requested', 'edit_file'): for x in ('delete_requested', 'reorder_spine', 'rename_requested', 'edit_file'):
getattr(self.file_list, x).connect(getattr(self, x)) getattr(self.file_list, x).connect(getattr(self, x))
for x in ('delete_done',): for x in ('delete_done', 'select_name'):
setattr(self, x, getattr(self.file_list, x)) setattr(self, x, getattr(self.file_list, x))
def build(self, container, preserve_state=True): def build(self, container, preserve_state=True):

View File

@ -0,0 +1,44 @@
#!/usr/bin/env python
# vim:fileencoding=utf-8
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
from calibre import prepare_string_for_xml
from calibre.gui2.tweak_book import current_container
DEFAULT_TEMPLATES = {
'html':
'''\
<?xml version='1.0' encoding='utf-8'?>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>{TITLE}</title>
</head>
<body>
%CURSOR%
</body>
</html>
''',
'css':
'''\
@charset utf-8;
/* Styles for {TITLE} */
%CURSOR%
''',
}
def template_for(syntax):
mi = current_container().mi
data = {
'TITLE':mi.title,
'AUTHOR': ' & '.join(mi.authors),
}
template = DEFAULT_TEMPLATES.get(syntax, '')
return template.format(**{k:prepare_string_for_xml(v, True) for k, v in data.iteritems()})

View File

@ -9,7 +9,7 @@ __copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
from functools import partial from functools import partial
from PyQt4.Qt import ( from PyQt4.Qt import (
QDockWidget, Qt, QLabel, QIcon, QAction, QApplication, QWidget, QDockWidget, Qt, QLabel, QIcon, QAction, QApplication, QWidget, QFontMetrics,
QVBoxLayout, QStackedWidget, QTabWidget, QImage, QPixmap, pyqtSignal) QVBoxLayout, QStackedWidget, QTabWidget, QImage, QPixmap, pyqtSignal)
from calibre.constants import __appname__, get_version from calibre.constants import __appname__, get_version
@ -22,6 +22,10 @@ from calibre.gui2.tweak_book.keyboard import KeyboardManager
from calibre.gui2.tweak_book.preview import Preview from calibre.gui2.tweak_book.preview import Preview
from calibre.gui2.tweak_book.search import SearchPanel from calibre.gui2.tweak_book.search import SearchPanel
def elided_text(font, text, width=200, mode=Qt.ElideMiddle):
fm = QFontMetrics(font)
return unicode(fm.elidedText(text, mode, int(width)))
class Central(QStackedWidget): class Central(QStackedWidget):
' The central widget, hosts the editors ' ' The central widget, hosts the editors '
@ -146,6 +150,9 @@ class Main(MainWindow):
self.keyboard.finalize() self.keyboard.finalize()
self.keyboard.set_mode('other') self.keyboard.set_mode('other')
def elided_text(self, text, width=200, mode=Qt.ElideMiddle):
return elided_text(self.font(), text, width=width, mode=mode)
@property @property
def editor_tabs(self): def editor_tabs(self):
return self.central.editor_tabs return self.central.editor_tabs
@ -165,6 +172,7 @@ class Main(MainWindow):
self.addAction(ac) self.addAction(ac)
return ac return ac
self.action_new_file = reg('document-new.png', _('&New file'), self.boss.add_file, 'new-file', (), _('Create a new file in the current book'))
self.action_open_book = reg('document_open.png', _('Open &book'), self.boss.open_book, 'open-book', 'Ctrl+O', _('Open a new book')) self.action_open_book = reg('document_open.png', _('Open &book'), self.boss.open_book, 'open-book', 'Ctrl+O', _('Open a new book'))
self.action_global_undo = reg('back.png', _('&Revert to before'), self.boss.do_global_undo, 'global-undo', 'Ctrl+Left', self.action_global_undo = reg('back.png', _('&Revert to before'), self.boss.do_global_undo, 'global-undo', 'Ctrl+Left',
_('Revert book to before the last action (Undo)')) _('Revert book to before the last action (Undo)'))
@ -245,6 +253,7 @@ class Main(MainWindow):
b = self.menuBar() b = self.menuBar()
f = b.addMenu(_('&File')) f = b.addMenu(_('&File'))
f.addAction(self.action_new_file)
f.addAction(self.action_open_book) f.addAction(self.action_open_book)
f.addAction(self.action_save) f.addAction(self.action_save)
f.addAction(self.action_quit) f.addAction(self.action_quit)
@ -302,7 +311,7 @@ class Main(MainWindow):
return b return b
a = create(_('Book tool bar'), 'global').addAction a = create(_('Book tool bar'), 'global').addAction
for x in ('open_book', 'global_undo', 'global_redo', 'save', 'create_checkpoint', 'toc'): for x in ('new_file', 'open_book', 'global_undo', 'global_redo', 'save', 'create_checkpoint', 'toc'):
a(getattr(self, 'action_' + x)) a(getattr(self, 'action_' + x))
a = create(_('Polish book tool bar'), 'polish').addAction a = create(_('Polish book tool bar'), 'polish').addAction