Original CC code, prepare for merge with beta tree

This commit is contained in:
Charles Haley 2010-05-01 17:44:49 +01:00
commit e7be95be7e
23 changed files with 4836 additions and 292 deletions

View File

@ -25,3 +25,20 @@ series_index_auto_increment = 'next'
# copy : copy author to author_sort without modification # copy : copy author to author_sort without modification
# comma : use 'copy' if there is a ',' in the name, otherwise use 'invert' # comma : use 'copy' if there is a ',' in the name, otherwise use 'invert'
author_sort_copy_method = 'invert' author_sort_copy_method = 'invert'
# Set whether boolean custom columns are two- or three-valued.
# Two-values for true booleans
# three-values for yes/no/unknown
# Set to 'yes' for three-values, 'no' for two-values
bool_custom_columns_are_tristate = 'yes'
# Provide a set of columns to be sorted on when calibre starts
# The argument is None of saved sort history is to be used
# otherwise it is a list of column,order pairs. Column is the
# lookup/search name, found using the tooltip for the column
# Order is 0 for ascending, 1 for descending
# For example, set it to [('authors',0),('title',0)] to sort by
# title within authors.
sort_columns_at_startup = None

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="500"
height="500"
id="svg3152"
version="1.1"
inkscape:version="0.47 r22583"
sodipodi:docname="New document 1">
<defs
id="defs3154">
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 526.18109 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="744.09448 : 526.18109 : 1"
inkscape:persp3d-origin="372.04724 : 350.78739 : 1"
id="perspective3160" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="1.0"
inkscape:pageopacity="1.0"
inkscape:pageshadow="0"
inkscape:zoom="0.34"
inkscape:cx="350"
inkscape:cy="520"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="643"
inkscape:window-height="666"
inkscape:window-x="125"
inkscape:window-y="125"
inkscape:window-maximized="0" />
<metadata
id="metadata3157">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-552.36218)" />
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,61 @@
<?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 height="253.5" id="Layer_1" inkscape:version="0.40+cvs" sodipodi:docbase="F:\openclip\svg3" sodipodi:docname="Capitello modanatura moulure.svg" sodipodi:version="0.32" style="overflow:visible;enable-background:new 0 0 277.433 253.5;" version="1.0" viewBox="0 0 277.433 253.5" width="277.433" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:sodipodi="http://inkscape.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg" xmlns:xml="http://www.w3.org/XML/1998/namespace"><metadata><rdf:RDF xmlns:cc="http://web.resource.org/cc/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><cc:Work rdf:about=""><dc:title>Capitello modanatura modanature moulure moulures</dc:title><dc:description></dc:description><dc:subject><rdf:Bag><rdf:li>building</rdf:li></rdf:Bag></dc:subject><dc:publisher><cc:Agent rdf:about="http://www.openclipart.org"><dc:title>Architetto Francesco Rollandin</dc:title></cc:Agent></dc:publisher><dc:creator><cc:Agent><dc:title>Architetto Francesco Rollandin</dc:title></cc:Agent></dc:creator><dc:rights><cc:Agent><dc:title>Architetto Francesco Rollandin</dc:title></cc:Agent></dc:rights><dc:date></dc:date><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><cc:license rdf:resource="http://web.resource.org/cc/PublicDomain"/><dc:language>en</dc:language></cc:Work><cc:License rdf:about="http://web.resource.org/cc/PublicDomain"><cc:permits rdf:resource="http://web.resource.org/cc/Reproduction"/><cc:permits rdf:resource="http://web.resource.org/cc/Distribution"/><cc:permits rdf:resource="http://web.resource.org/cc/DerivativeWorks"/></cc:License></rdf:RDF></metadata>
<defs id="defs56"></defs>
<sodipodi:namedview bordercolor="#666666" borderopacity="1.0" id="base" inkscape:current-layer="Layer_1" inkscape:cx="138.71651" inkscape:cy="126.75000" inkscape:pageopacity="0.0" inkscape:pageshadow="2" inkscape:window-height="540" inkscape:window-width="640" inkscape:window-x="22" inkscape:window-y="22" inkscape:zoom="1.4911243" pagecolor="#ffffff"></sodipodi:namedview>
<g id="g3">
<path d="M269.987,16.446c11.514,10.547,7.726,33.31,1.663,45.651c-9.559,19.459-32.822,21.546-51.841,19.009 c-2.021,22.636-2.369,45.377-2.399,68.09c-0.015,10.891,0.025,21.781,0.025,32.672c0,5.381,0,10.762,0,16.143 c0,4.35,1.145,5.354,5.544,5.098c2.975-0.172,12.557-3.239,12.893,1.706c0.221,3.245-2.241,9.014-0.016,11.713 c1.997,2.421,5.807,3.181,8.67,3.993c3.729,1.058,7.776,3.027,11.667,3.265c2.984,0.182,1.435,5.441,1.368,7.535 c-0.148,4.68,0.078,9.268,0.319,13.928c0.24,4.638-1.359,4.189-5.543,4.594c-21.966,2.125-44.538,0.341-66.583,0.291 c-22.807-0.052-45.619,0.078-68.409,1.014c-22.082,0.906-44.168,0.835-66.252,1.523c-9,0.28-18.09,0.771-27.071,0.831 c-3.313,0.022-3.382-1.347-2.935-4.079c0.726-4.434,0.467-8.914,0.467-13.398c0-2.373-1.074-7.706,1.34-9.398 c2.938-2.061,7.151-2.742,10.401-4.265c9.135-4.276,6.441-7.791,6.591-16.34c0.073-4.137,5.426-1.096,8.395-1.154 c4.361-0.087,2.593-4.402,2.559-7.859c-0.064-6.333-0.206-12.665-0.382-18.995c-0.639-23.031-1.182-46.073-2.019-69.097 c-0.196-5.4-0.352-10.801-0.472-16.204c-0.058-2.589,1.452-11.419-2.132-11.275c-8.722,0.349-17.113-0.684-25.05-4.56 C-12.651,60.547-3.602,8.372,32.616,2.474c9.309-1.516,19.017-0.612,28.399-0.511c11.773,0.126,23.546,0.163,35.32,0.157 c23.119-0.013,46.238-0.115,69.354-0.556C185.921,1.177,206.175-0.019,226.411,0C242.655,0.015,259.596,3.013,269.987,16.446" id="path5"></path>
<path d="M263.497,12.805c17.229,17.231,13.408,52.826-9.883,63.111c-11.111,4.906-29.468,6.652-39.829-0.784 c-4.768-3.42-10.636-8.188-13.193-13.588c-2.79-5.893-0.708-14.659,1.955-20.272c5.144-10.839,15.963-19.15,28.248-18.245 c13.067,0.963,21.35,10.229,19.214,23.444c-1.684,10.42-12.965,22.841-24.252,15.978c-4.984-3.032-8.286-9.521-6.396-15.307 c1.08-3.303,8.425-8.779,10.105-2.996c-3.617-0.933-3.468,5.562-2.88,7.464c1.694,5.481,8.471,5.282,12.069,1.689 c10.924-10.907-4.807-28.457-17.827-22.066c-15.255,7.487-15.464,27.982-1.153,36.475c13.494,8.008,31.806,1.371,39.313-11.699 c6.751-11.753,4.543-29.552-5.98-38.545c-6.412-5.48-16.133-5.78-24.13-6.6C217.514,9.7,206.07,9.611,194.659,9.88 c-45.654,1.077-91.252,2.635-136.93,2.865c-10.516,0.053-26.706-3.068-35.7,3.59C13.072,22.966,7.624,34.89,8.907,45.965 c2.465,21.27,26.852,34.439,45.764,23.854C63.05,65.13,69.272,56.151,68.154,46.26c-1.048-9.266-8.71-17.296-18.334-17.278 c-10.437,0.02-18.804,10.469-13.622,20.189c2.062,3.867,8.494,5.687,11.636,1.998c2.241-2.63,1.254-7.959-2.931-7.419 c4.833-5.223,11.914,1.513,11.38,7.142c-0.623,6.566-7.648,11.135-13.593,12.024c-15.159,2.267-23.777-11.625-20.532-25.271 c3.998-16.809,21.359-21.542,36.573-17.014c17.902,5.329,19.484,20.76,13.54,36.295C59.813,89.49,7.194,84.666,2.164,49.499 c-2.4-16.776,6.42-36.324,22.822-42.628c7.058-2.713,14.77-2.768,22.225-2.692C58.154,4.29,69.098,4.427,80.042,4.491 c32.615,0.19,65.303,0.147,97.908-0.728c16.363-0.439,32.709-1.273,49.082-0.806C239.347,3.309,254.146,3.507,263.497,12.805" id="path7" style="fill:#BFBFBF;"></path>
<path d="M258.749,31.246c3.035,11.892-0.551,27.027-11.674,33.584c-10.348,6.101-28.37,4.505-33.847-7.551 c-4.545-10.002,2.436-25.982,14.791-24.727c6.887,0.7,10.927,5.838,10.588,12.623c-0.167,3.326-0.945,6.588-4.631,7.439 c-3.448,0.796-6.992-1.519-4.906-5.223c4.045,5.535,5.103-6.75-1.358-8.312c-6.246-1.511-10.79,3.854-10.992,9.564 c-0.493,13.941,14.36,23.351,26.353,15.28c11.628-7.826,13.93-28.233,3.285-37.841c-6.293-5.681-15.592-6.508-23.673-6.923 c-11.101-0.57-22.227,0.536-33.31,1.041c-22.333,1.019-44.661,2.149-66.996,3.14c-10.522,0.466-21.048,0.829-31.577,1.124 c-5.074,0.142-10.148,0.27-15.223,0.378c-5.138,0.109-9.622-3.251-14.107-5.271c-13.721-6.181-33.438-2.347-40.341,11.952 c-5.646,11.696-1.719,30.13,12.218,33.742c10.756,2.788,31.213-5.459,25.068-19.708c-2.384-5.527-9.835-7.564-14.897-4.53 c-1.53,0.917-5.698,8.159-1.748,6.881c1.042-0.337,1.828-1.721,3.053-1.544c2.031,0.294,1.867,1.657,0.453,2.672 c-6.196,4.449-9.433-4.01-7.865-8.951c2.52-7.944,13.175-10.17,19.805-6.519c13.573,7.474,8.818,25.74-2.296,32.597 c-14.444,8.912-34.788,3.045-41.537-12.791C7.439,39.422,15.236,19.093,30.86,15.584c9.506-2.136,20.457-0.466,30.129-0.384 c12.077,0.103,24.155,0.117,36.232,0.044c23.102-0.14,46.172-0.626,69.248-1.748c19.153-0.93,38.758-2.352,57.946-1.18 C237.174,13.095,255.954,15.957,258.749,31.246" id="path9" style="fill:#808080;"></path>
<path d="M216.96,22.065c-4.189,1.457-6.926,5.1-10.035,8.035c-3.926,3.706-7.758,3.477-12.894,3.057 c-13.75-1.124-27.989-0.009-41.786,0.081c-14.754,0.095-29.439,0.667-44.163,1.609c-7.355,0.471-14.71,0.961-22.071,1.337 c-3.388,0.173-7.721,1.855-8.424-1.65c-0.532-2.656-2.151-4.901-3.401-7.245c25.986-0.478,51.909-1.521,77.857-2.958 c12.748-0.706,25.585-1.691,38.35-1.883c5.152-0.078,10.293,0.221,15.423-0.352C209.098,21.729,213.818,20.556,216.96,22.065" id="path11" style="fill:#5E5E5E;"></path>
<path d="M202.556,35.282c-3.06,6.708-6.111,13.132-5.917,20.674c0.065,2.534,0.354,5.179,1.679,7.404 c1.462,2.459,4.185,3.993,4.92,6.893c1.413,5.582-0.519,12.133-0.678,17.817c-0.233,8.271-0.503,16.542-0.691,24.813 c-0.373,16.291-0.483,32.586-0.646,48.88c-0.081,8.149-0.175,16.3-0.321,24.449c-0.058,3.206,1.931,16.342-2.582,17.381 c-4.207,0.97-9.048,1.308-9.19-3.83c-0.206-7.431,0.059-14.804,0.187-22.237c0.296-17.278,0.149-34.562,0.442-51.841 c0.278-16.383,0.703-32.763,1.037-49.146c0.156-7.656,0.222-15.316,0.411-22.972c0.161-6.488-5.237-11.803-7.485-17.813 C189.794,33.726,196.337,35.792,202.556,35.282" id="path13" style="fill:#E3E3E3;"></path>
<path d="M149.926,35.678c-1.179,5.098-7.83,6.24-8.979,10.95c-1.648,6.761-0.583,15.063-0.745,22.007 c-0.406,17.411-0.884,34.822-0.57,52.239c0.308,17.057,0.717,34.095,0.717,51.156c0,8.01,0,16.02,0,24.028 c0,7.207,0.133,7.996-7.093,7.5c-6.914-0.475-3.747-15.828-3.729-20.809c0.059-16.925,0.119-33.849,0.178-50.773 c0.061-17.404-0.096-34.828,0.439-52.226c0.247-8.041,0.696-16.035,0.593-24.084c-0.11-8.56-2.827-11.851-8.037-18.326 C131.735,36.181,140.884,36.717,149.926,35.678" id="path15" style="fill:#E3E3E3;"></path>
<path d="M180.872,35.678c-0.979,4.869-5.544,7.579-8.596,11.063c-4.358,4.976-2.346,12.479-2.414,18.523 c-0.192,17.04-1.516,33.989-1.317,51.069c0.191,16.582,0.597,33.16,0.705,49.742c0.054,8.282,0.034,16.565-0.123,24.847 c-0.069,3.652,0.211,7.544-0.281,11.169c-0.41,3.021-5.537,2.033-7.736,2.147c-1.793,0.094-1.443-80.056-1.404-87.238 c0.085-16.094,0.108-32.21,0.636-48.297c0.19-5.829,0.481-11.56,1.162-17.345c0.758-6.434-3.308-10.796-7.856-15.206 c3.301-1.833,8.131-0.664,11.682-0.447C170.432,36.017,175.776,36.123,180.872,35.678" id="path17" style="fill:#E3E3E3;"></path>
<path d="M118.269,37.735c-1.283,2.154-2.983,4.106-4.986,5.619c-1.565,1.183-3.853,1.641-4.952,3.395 c-1.47,2.347,0.599,5.242,0.599,7.685c0,4.216-0.604,8.428-0.717,12.647c-0.438,16.338-0.463,32.68-0.272,49.021 c0.194,16.553,0.592,33.098,1.683,49.619c0.389,5.881,5.109,37.916-2.616,38.886c-3.479,0.438-8.817,1.344-9.113-3.157 c-0.499-7.577-0.5-15.216-0.751-22.806c-0.55-16.556-1.148-33.111-1.437-49.675c-0.284-16.31-0.281-32.628,0.23-48.934 c0.255-8.16,0.637-16.315,1.172-24.461c0.475-7.23-3.972-10.534-5.669-17.048C100.385,38.349,109.315,37.358,118.269,37.735" id="path19" style="fill:#E3E3E3;"></path>
<path d="M158.395,46.125c-0.533,31.886-1.188,63.758-1.188,95.65c0,15.317,0,30.634,0,45.95 c0,4.617,0.757,9.24,0.664,13.833c-0.068,3.371-8.199,3.735-8.344,0.423c-1.413-32.226-1.844-64.548-1.556-96.803 c0.143-15.907,0.771-31.809,1.218-47.709c0.135-4.806-1.466-13.862,1.021-18.2C152.52,35.243,157.134,45.118,158.395,46.125" id="path21" style="fill:#969696;"></path>
<path d="M86.137,40.11c-1.802,4.444-6.351,6.834-8.785,10.843c0.118-3.233-1.075-7.139,0.521-10.162 C78.82,38.997,86.6,37.518,86.137,40.11" id="path23" style="fill:#E3E3E3;"></path>
<path d="M127.924,47.708c-0.864,29.137-0.867,58.291-0.963,87.438c-0.048,14.845-0.097,29.689-0.146,44.534 c-0.023,6.938,0.175,13.919-0.089,20.853c-0.133,3.509-2.079,3.183-5.074,3.256c-3.186,0.077-1.998-5.488-2.133-8.231 c-0.182-3.69-0.362-7.381-0.538-11.071c-0.351-7.383-0.684-14.767-0.974-22.152c-0.581-14.771-0.985-29.548-1.057-44.33 c-0.071-14.73,0.919-29.391,1.103-44.1c0.088-7.041-0.18-14.086-0.18-21.128c0-5.268-1.026-9.582,2.85-13.457 C123.231,42.02,125.295,45.115,127.924,47.708" id="path25" style="fill:#969696;"></path>
<path d="M188.469,83.005c-0.868,27.782-1.484,55.573-1.786,83.367c-0.124,11.389,0.632,23.096-0.191,34.44 c-0.213,2.944-4.001,4.863-6.649,2.729c-2.821-2.274-2.137-11.711-2.193-14.737c-0.244-12.991,0.978-25.921,0.54-38.927 c-0.438-12.993-0.009-25.956,0.292-38.947c0.301-13.003,0.601-26.007,0.907-39.01c0.25-10.571-2.265-22.762,2.749-32.602 C192.517,51.294,189.273,68.576,188.469,83.005" id="path27" style="fill:#969696;"></path>
<path d="M95.001,51.744c-1.75,28.572-2.194,57.212-1.738,85.831c0.216,13.561,0.746,27.112,1.23,40.665 c0.248,6.934,0.492,13.867,0.735,20.801c0.229,6.533-2.363,4.685-7.43,5.211c-1.578-28.564-2.779-57.147-2.978-85.757 c-0.093-13.326-0.059-26.656,0.183-39.98c0.121-6.69,0.447-13.376,0.565-20.066c0.106-6.027,0.059-12.19,3.418-17.468 C90.991,44.568,92.996,48.156,95.001,51.744" id="path29" style="fill:#969696;"></path>
<path d="M145.177,87.437c0.988,25.058,1.155,50.139,1.183,75.214c0.013,11.358,0.005,22.718,0.005,34.077 c0,1.782,0.651,4.979-0.428,6.579c-2.152,3.19-2.427-2.293-2.446-2.981c-0.175-6.101-0.312-12.202-0.37-18.306 c-0.233-24.164-0.411-48.273-0.979-72.433c-0.275-11.732,0.069-23.448,0.522-35.172c0.243-6.293,0.513-12.586,0.71-18.882 c0.128-4.116-0.891-8.615,2.99-11.388C146.64,58.581,146.323,73.043,145.177,87.437" id="path31" style="fill:#BFBFBF;"></path>
<path d="M115.42,50.953c-1.174,28.427-1.397,56.849-0.762,85.292c0.306,13.686,0.66,27.37,1.144,41.051 c0.247,6.976,0.493,13.949,0.709,20.926c0.113,3.651,0.938,5.553-3.386,6.031c-0.947-29.533-2.22-59.063-2.748-88.608 c-0.248-13.869-0.502-27.894,0.314-41.748c0.369-6.252,1.427-12.552,0.738-18.821c-0.473-4.301-1.087-7.463,3.278-9.742 C114.97,47.199,115.34,49.065,115.42,50.953" id="path33" style="fill:#BFBFBF;"></path>
<path d="M177.626,47.312c-1.061,29.439-2.265,58.83-1.941,88.295c0.16,14.534-0.268,29.064-0.391,43.597 c-0.057,6.632-0.034,13.265,0.191,19.893c0.047,1.372,0.716,3.423-0.136,4.674c-0.996,1.462-2.972,0.332-3.359-0.947 c-1.224-4.041-0.131-9.819-0.212-13.987c-0.144-7.308-0.277-14.616-0.389-21.924c-0.224-14.616-0.359-29.235-0.309-43.853 c0.051-14.634,0.195-29.277,0.585-43.906c0.269-10.124-2.24-25.542,5.248-33.028C177.136,46.464,177.512,46.979,177.626,47.312" id="path35" style="fill:#BFBFBF;"></path>
<path d="M82.18,85.063c0.151,25.021-0.078,50.047,0.583,75.063c0.338,12.787,1.207,25.561,1.396,38.35 c0.032,2.203,1.505,6.316-2.127,5.501c-1.73-0.388-1.261-14.663-1.35-16.759c-1.034-24.321-2.365-48.646-2.663-72.991 c-0.149-12.221-0.477-24.412,0.259-36.618c0.576-9.556-1.54-21.67,4.694-29.426C82.115,60.46,82.981,72.783,82.18,85.063" id="path37" style="fill:#BFBFBF;"></path>
<path d="M76.56,148.457c0.157,10.251,0.532,20.496,0.961,30.738c0.296,7.057,2.459,16.297,1.102,23.311 c-0.727,3.755-9.991,3.729-10.081-0.096c-0.255-10.852-0.904-21.675-1.379-32.518c-0.936-21.354-1.054-42.732-1.057-64.104 c-0.001-9.405-0.544-18.909-0.314-28.295c0.123-5.044,2.847-7.453,5.167-11.591c2.299-4.102,3.548-8.677,5.603-12.893 C75.467,84.813,75.458,116.654,76.56,148.457" id="path39" style="fill:#E3E3E3;"></path>
<path d="M207.781,73.825c0.317,21.825-0.792,43.493-0.792,65.278c0,10.738,0,21.478,0,32.216 c0,5.481,0,10.963,0,16.445c0,2.65,1.686,12.848-0.612,14.655c-4.082,3.21-2.532-8.896-2.516-10.782 c0.052-5.517,0.092-11.034,0.129-16.552c0.074-11.035,0.133-22.071,0.23-33.106c0.202-22.987,0.569-45.978,1.582-68.945 C206.461,73.297,207.122,73.561,207.781,73.825" id="path41" style="fill:#BFBFBF;"></path>
<path d="M63.66,154.472c0.783,16.721,1.787,33.437,2.058,50.177c-2.401-0.132-4.802-0.265-7.203-0.396 c4.111-2.813,0.355-17.984,0.207-22.342c-0.486-14.264-0.922-28.527-1.294-42.794c-0.371-14.189-1.13-28.355-1.666-42.537 c-0.121-3.195-2.525-15.268-0.633-17.566c1.9-2.309,5.626-3.449,8.214-4.792c0.066,13.375,0.132,26.75,0.198,40.125 C63.607,127.713,64.891,141.124,63.66,154.472" id="path43" style="fill:#BFBFBF;"></path>
<path d="M216.96,80.235c0.446,21.664-0.711,43.313-1.313,64.962c-0.296,10.651-0.357,21.305-0.45,31.959 c-0.03,3.503,1.627,25.193-1.97,25.515c-5.911,0.527-3.371-20.418-3.351-24.031c0.063-10.85,0.132-21.698,0.181-32.548 c0.104-23.14,0.117-46.282,0.571-69.418C213.076,77.254,214.519,79.697,216.96,80.235" id="path45" style="fill:#E3E3E3;"></path>
<path d="M52.421,79.84c0.88,22.406,1.916,44.805,2.742,67.213c0.432,11.725,0.802,23.45,1.042,35.18 c0.088,4.317-2.453,18.714,1.124,22.02c-4.652,0-3.771-2.246-3.999-6.369c-0.426-7.703-0.767-15.425-0.831-23.14 c-0.122-14.887-0.063-29.809-0.743-44.682C51.631,127.329,46.842,79.502,52.421,79.84" id="path47" style="fill:#E3E3E3;"></path>
<path d="M233.027,205.044c2.539,9.07-2.377,10.524-9.861,11.079c-9.534,0.707-19.151,0.729-28.707,0.77 c-20.313,0.088-40.627,0.037-60.94,0.133c-19.374,0.091-38.575,1.29-57.922,1.899c-9.106,0.287-18.38,0.342-27.438-0.75 c-3.971-0.479-5.018,0.146-5.204-3.817c-0.107-2.286-0.019-4.574-0.109-6.859c31.892,0,63.831-1.94,95.708-1.301 c16.213,0.325,32.394,0.372,48.607,0.431C202.487,206.683,217.678,205.044,233.027,205.044" id="path49" style="fill:#BFBFBF;"></path>
<path d="M249.092,224.275c-37.66,2.193-75.668-0.496-113.257,2.421c-36.526,2.834-73.583,2.146-110.243,1.22 c14.839-9.382,31.379-6.451,47.913-6.075c20.089,0.457,40.228-2.287,60.35-2.391c20.478-0.105,40.953,0.454,61.432,0.33 c10.018-0.061,19.98-0.497,29.972-1.229C234.292,217.891,240.738,221.712,249.092,224.275" id="path51" style="fill:#E3E3E3;"></path>
<path d="M255.108,245.961c-1.281,0.09-1.54,1.658-2.793,1.662c-2.189,0.008-4.379,0.015-6.568,0.022 c-5.926,0.021-11.852,0.04-17.777,0.061c-11.854,0.041-23.707,0.08-35.561,0.121c-23.952,0.081-47.894,0.144-71.839,0.773 c-23.992,0.63-47.979,1.343-71.98,1.604c-6.661,0.072-13.326,0.174-19.988,0.186c-3.327,0.005-4.571,1.054-4.644-2.605 c-0.115-5.806-0.229-11.61-0.344-17.415c23.308,2.299,47.056,0.97,70.449,1.107c23.82,0.141,47.644-2.438,71.469-3.03 c23.765-0.591,47.532-0.933,71.303-0.928c3.688,0.001,17.249-3.522,18.395,1.103C256.617,234.223,255.108,240.288,255.108,245.961" id="path53" style="fill:#BFBFBF;"></path>
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

2679
resources/images/drawer.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 278 KiB

View File

@ -13,6 +13,7 @@ from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
ORG_NAME = 'KovidsBrain' ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500' APP_UID = 'libprs500'
from calibre import islinux, iswindows, isosx, isfreebsd from calibre import islinux, iswindows, isosx, isfreebsd
from calibre.constants import preferred_encoding
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
from calibre.utils.localization import set_qt_translator from calibre.utils.localization import set_qt_translator
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
@ -95,6 +96,8 @@ def _config():
help=_('Overwrite author and title with new metadata')) help=_('Overwrite author and title with new metadata'))
c.add_opt('enforce_cpu_limit', default=True, c.add_opt('enforce_cpu_limit', default=True,
help=_('Limit max simultaneous jobs to number of CPUs')) help=_('Limit max simultaneous jobs to number of CPUs'))
c.add_opt('user_categories', default={},
help=_('User-created tag browser categories'))
return ConfigProxy(c) return ConfigProxy(c)

View File

@ -0,0 +1,17 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import QDialog
from calibre.gui2 import ResizableDialog
from calibre.gui2.dialogs.comments_dialog_ui import Ui_CommentsDialog
class CommentsDialog(QDialog, Ui_CommentsDialog):
def __init__(self, parent, text):
QDialog.__init__(self, parent)
Ui_CommentsDialog.__init__(self)
self.setupUi(self)
if text is not None:
self.textbox.setPlainText(text)
self.textbox.setTabChangesFocus(True)

View File

@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>CommentsDialog</class>
<widget class="QDialog" name="CommentsDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>336</width>
<height>235</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Edit Comments</string>
</property>
<widget class="QWidget" name="verticalLayoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>10</y>
<width>311</width>
<height>211</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QPlainTextEdit" name="textbox"/>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>CommentsDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>229</x>
<y>211</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>234</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>CommentsDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>297</x>
<y>217</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>234</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1,6 +1,6 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, re, time, textwrap import os, re, time, textwrap, sys, copy
from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \ from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \ QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
@ -8,10 +8,11 @@ from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
SIGNAL, QThread, Qt, QSize, QVariant, QUrl, \ SIGNAL, QThread, Qt, QSize, QVariant, QUrl, \
QModelIndex, QAbstractTableModel, \ QModelIndex, QAbstractTableModel, \
QDialogButtonBox, QTabWidget, QBrush, QLineEdit, \ QDialogButtonBox, QTabWidget, QBrush, QLineEdit, \
QProgressDialog QProgressDialog, QMessageBox
from calibre.constants import iswindows, isosx from calibre.constants import iswindows, isosx, preferred_encoding
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, \ from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, \
ALL_COLUMNS, NONE, info_dialog, choose_files, \ ALL_COLUMNS, NONE, info_dialog, choose_files, \
warning_dialog, ResizableDialog warning_dialog, ResizableDialog
@ -90,7 +91,6 @@ class ConfigTabs(QTabWidget):
widget.commit(save_defaults=True) widget.commit(save_defaults=True)
return True return True
class PluginModel(QAbstractItemModel): class PluginModel(QAbstractItemModel):
def __init__(self, *args): def __init__(self, *args):
@ -328,14 +328,16 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
def category_current_changed(self, n, p): def category_current_changed(self, n, p):
self.stackedWidget.setCurrentIndex(n.row()) self.stackedWidget.setCurrentIndex(n.row())
def __init__(self, window, db, server=None): def __init__(self, parent, model, server=None):
ResizableDialog.__init__(self, window) ResizableDialog.__init__(self, parent)
self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)} self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
self._category_model = CategoryModel() self._category_model = CategoryModel()
self.category_view.currentChanged = self.category_current_changed self.category_view.currentChanged = self.category_current_changed
self.category_view.setModel(self._category_model) self.category_view.setModel(self._category_model)
self.db = db self.parent = parent
self.model = model
self.db = model.db
self.server = server self.server = server
path = prefs['library_path'] path = prefs['library_path']
self.location.setText(path if path else '') self.location.setText(path if path else '')
@ -359,15 +361,27 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.roman_numerals.setChecked(rn) self.roman_numerals.setChecked(rn)
self.new_version_notification.setChecked(config['new_version_notification']) self.new_version_notification.setChecked(config['new_version_notification'])
column_map = config['column_map'] # Set up columns
for col in column_map + [i for i in ALL_COLUMNS if i not in column_map]: # Make copies of maps so that internal changes aren't put into the real maps
item = QListWidgetItem(BooksModel.headers[col], self.columns) self.colmap = config['column_map'][:]
self.custcols = copy.deepcopy(self.db.custom_column_label_map)
cm = [c.decode(preferred_encoding, 'replace') for c in self.colmap]
ac = [c.decode(preferred_encoding, 'replace') for c in ALL_COLUMNS]
for col in cm + \
[i for i in ac if i not in cm] + \
[i for i in self.custcols if i not in cm]:
if col in ALL_COLUMNS:
item = QListWidgetItem(model.headers[col], self.columns)
else:
item = QListWidgetItem(self.custcols[col]['name'], self.columns)
item.setData(Qt.UserRole, QVariant(col)) item.setData(Qt.UserRole, QVariant(col))
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable) item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
item.setCheckState(Qt.Checked if col in column_map else Qt.Unchecked) item.setCheckState(Qt.Checked if col in self.colmap else Qt.Unchecked)
self.connect(self.column_up, SIGNAL('clicked()'), self.up_column) self.connect(self.column_up, SIGNAL('clicked()'), self.up_column)
self.connect(self.column_down, SIGNAL('clicked()'), self.down_column) self.connect(self.column_down, SIGNAL('clicked()'), self.down_column)
self.connect(self.del_custcol_button, SIGNAL('clicked()'), self.del_custcol)
self.connect(self.add_custcol_button, SIGNAL('clicked()'), self.add_custcol)
self.connect(self.edit_custcol_button, SIGNAL('clicked()'), self.edit_custcol)
icons = config['toolbar_icon_size'] icons = config['toolbar_icon_size']
self.toolbar_button_size.setCurrentIndex(0 if icons == self.ICON_SIZES[0] else 1 if icons == self.ICON_SIZES[1] else 2) self.toolbar_button_size.setCurrentIndex(0 if icons == self.ICON_SIZES[0] else 1 if icons == self.ICON_SIZES[1] else 2)
@ -398,7 +412,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
for item in items: for item in items:
self.language.addItem(item[1], QVariant(item[0])) self.language.addItem(item[1], QVariant(item[0]))
exts = set([]) exts = set([])
for ext in BOOK_EXTENSIONS: for ext in BOOK_EXTENSIONS:
ext = ext.lower() ext = ext.lower()
@ -633,6 +646,31 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.columns.insertItem(idx+1, self.columns.takeItem(idx)) self.columns.insertItem(idx+1, self.columns.takeItem(idx))
self.columns.setCurrentRow(idx+1) self.columns.setCurrentRow(idx+1)
def del_custcol(self):
idx = self.columns.currentRow()
if idx < 0:
self.messagebox(_('You must select a column to delete it'))
return
col = qstring_to_unicode(self.columns.item(idx).data(Qt.UserRole).toString())
if col not in self.custcols:
self.messagebox(_('The selected column is not a custom column'))
return
ret = self.messagebox(_('Do you really want to delete column %s and all its data')%self.custcols[col]['name'],
buttons=QMessageBox.Ok|QMessageBox.Cancel,
defaultButton=QMessageBox.Cancel)
if ret != QMessageBox.Ok:
return
self.columns.item(idx).setCheckState(False)
self.columns.takeItem(idx)
self.custcols[col]['*deleteme'] = True
return
def add_custcol(self):
d = CreateCustomColumn(self, False, self.model.orig_headers, ALL_COLUMNS)
def edit_custcol(self):
d = CreateCustomColumn(self, True, self.model.orig_headers, ALL_COLUMNS)
def view_server_logs(self): def view_server_logs(self):
from calibre.library.server import log_access_file, log_error_file from calibre.library.server import log_access_file, log_error_file
d = QDialog(self) d = QDialog(self)
@ -702,7 +740,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
if dir: if dir:
self.location.setText(dir) self.location.setText(dir)
def accept(self): def accept(self):
mcs = unicode(self.max_cover_size.text()).strip() mcs = unicode(self.max_cover_size.text()).strip()
if not re.match(r'\d+x\d+', mcs): if not re.match(r'\d+x\d+', mcs):
@ -720,17 +757,38 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
wl += 1 wl += 1
config['worker_limit'] = wl config['worker_limit'] = wl
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked()) config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
config['new_version_notification'] = bool(self.new_version_notification.isChecked()) config['new_version_notification'] = bool(self.new_version_notification.isChecked())
prefs['network_timeout'] = int(self.timeout.value()) prefs['network_timeout'] = int(self.timeout.value())
path = qstring_to_unicode(self.location.text()) path = qstring_to_unicode(self.location.text())
input_cols = [unicode(self.input_order.item(i).data(Qt.UserRole).toString()) for i in range(self.input_order.count())] input_cols = [unicode(self.input_order.item(i).data(Qt.UserRole).toString()) for i in range(self.input_order.count())]
prefs['input_format_order'] = input_cols prefs['input_format_order'] = input_cols
cols = [unicode(self.columns.item(i).data(Qt.UserRole).toString()) for i in range(self.columns.count()) if self.columns.item(i).checkState()==Qt.Checked]
####### Now deal with changes to columns
cols = [qstring_to_unicode(self.columns.item(i).data(Qt.UserRole).toString())\
for i in range(self.columns.count()) \
if self.columns.item(i).checkState()==Qt.Checked]
if not cols: if not cols:
cols = ['title'] cols = ['title']
config['column_map'] = cols config['column_map'] = cols
must_restart = False
for c in self.custcols:
if self.custcols[c]['num'] is None:
self.db.create_custom_column(
label=c,
name=self.custcols[c]['name'],
datatype=self.custcols[c]['datatype'],
is_multiple=self.custcols[c]['is_multiple'])
must_restart = True
elif '*deleteme' in self.custcols[c]:
self.db.delete_custom_column(label=c)
must_restart = True
elif '*edited' in self.custcols[c]:
cc = self.custcols[c]
self.db.set_custom_column_metadata(cc['num'], name=cc['name'], label=cc['label'])
if '*must_restart' in self.custcols[c]:
must_restart = True
config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()] config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked()) config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
config['separate_cover_flow'] = bool(self.separate_cover_flow.isChecked()) config['separate_cover_flow'] = bool(self.separate_cover_flow.isChecked())
@ -771,8 +829,16 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
d.exec_() d.exec_()
else: else:
self.database_location = os.path.abspath(path) self.database_location = os.path.abspath(path)
if must_restart:
self.messagebox(_('The changes you made require that Calibre be restarted. Please restart as soon as practical.'))
self.parent.must_restart_before_config = True
QDialog.accept(self) QDialog.accept(self)
# might want to substitute the standard calibre box. However, the copy_to_clipboard
# functionality has no purpose, so ???
def messagebox(self, m, buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok):
return QMessageBox.critical(None,'Calibre configuration', m, buttons, defaultButton)
class VacThread(QThread): class VacThread(QThread):
def __init__(self, parent, db): def __init__(self, parent, db):

View File

@ -498,6 +498,87 @@
</property> </property>
</spacer> </spacer>
</item> </item>
<item>
<widget class="QToolButton" name="del_custcol_button">
<property name="toolTip">
<string>Remove a user-defined column</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../../resources/images.qrc">
<normaloff>:/images/minus.svg</normaloff>:/images/minus.svg</iconset>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="add_custcol_button">
<property name="text">
<string>...</string>
</property>
<property name="toolTip">
<string>Add a user-defined column</string>
</property>
<property name="icon">
<iconset resource="../../../../../resources/images.qrc">
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="edit_custcol_button">
<property name="toolTip">
<string>Edit settings of a user-defined column</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../../resources/images.qrc">
<normaloff>:/images/edit_input.svg</normaloff>:/images/edit_input.svg</iconset>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item> <item>
<widget class="QToolButton" name="column_down"> <widget class="QToolButton" name="column_down">
<property name="text"> <property name="text">

View File

@ -0,0 +1,123 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid at kovidgoyal.net>'
'''Dialog to create a new custom column'''
from PyQt4.QtCore import SIGNAL, QObject
from PyQt4.Qt import QDialog, Qt, QMessageBox, QListWidgetItem, QVariant
from calibre.gui2.dialogs.config.create_custom_column_ui import Ui_QCreateCustomColumn
from calibre.gui2 import ALL_COLUMNS, qstring_to_unicode
class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
column_types = {
0:{'datatype':'text', 'text':_('Text, column shown in tags browser'), 'is_multiple':False},
1:{'datatype':'*text', 'text':_('Comma separated text, shown in tags browser'), 'is_multiple':True},
2:{'datatype':'comments', 'text':_('Text, column not shown in tags browser'), 'is_multiple':False},
3:{'datatype':'datetime', 'text':_('Date'), 'is_multiple':False},
4:{'datatype':'float', 'text':_('Float'), 'is_multiple':False},
5:{'datatype':'int', 'text':_('Integer'), 'is_multiple':False},
6:{'datatype':'rating', 'text':_('Rating (stars)'), 'is_multiple':False},
7:{'datatype':'bool', 'text':_('Yes/No'), 'is_multiple':False},
}
def __init__(self, parent, editing, standard_colheads, standard_colnames):
QDialog.__init__(self, parent)
Ui_QCreateCustomColumn.__init__(self)
self.setupUi(self)
self.connect(self.button_box, SIGNAL("accepted()"), self.accept)
self.connect(self.button_box, SIGNAL("rejected()"), self.reject)
self.parent = parent
self.editing_col = editing
self.standard_colheads = standard_colheads
self.standard_colnames = standard_colnames
if not self.editing_col:
for t in self.column_types:
self.column_type_box.addItem(self.column_types[t]['text'])
self.exec_()
return
idx = parent.columns.currentRow()
if idx < 0:
self.parent.messagebox(_('No column has been selected'))
return
col = qstring_to_unicode(parent.columns.item(idx).data(Qt.UserRole).toString())
if col not in parent.custcols:
self.parent.messagebox(_('Selected column is not a user-defined column'))
return
c = parent.custcols[col]
self.column_name_box.setText(c['label'])
self.column_heading_box.setText(c['name'])
ct = c['datatype'] if not c['is_multiple'] else '*text'
self.orig_column_number = c['num']
self.orig_column_name = col
column_numbers = dict(map(lambda x:(self.column_types[x]['datatype'], x), self.column_types))
self.column_type_box.addItem(self.column_types[column_numbers[ct]]['text'])
self.exec_()
def accept(self):
col = qstring_to_unicode(self.column_name_box.text())
col_heading = qstring_to_unicode(self.column_heading_box.text())
col_type = self.column_types[self.column_type_box.currentIndex()]['datatype']
if col_type == '*text':
col_type='text'
is_multiple = True
else:
is_multiple = False
if not col:
self.parent.messagebox(_('No lookup name was provided'))
return
if not col_heading:
self.parent.messagebox(_('No column heading was provided'))
return
bad_col = False
if col in self.parent.custcols:
if not self.editing_col or self.parent.custcols[col]['num'] != self.orig_column_number:
bad_col = True
if col in self.standard_colnames:
bad_col = True
if bad_col:
self.parent.messagebox(_('The lookup name %s is already used')%col)
return
bad_head = False
for t in self.parent.custcols:
if self.parent.custcols[t]['name'] == col_heading:
if not self.editing_col or self.parent.custcols[t]['num'] != self.orig_column_number:
bad_head = True
for t in self.standard_colheads:
if self.standard_colheads[t] == col_heading:
bad_head = True
if bad_head:
self.parent.messagebox(_('The heading %s is already used')%col_heading)
return
if col.find(':') >= 0 or col.find(' ') >= 0 and \
(not is_alpha(col) or is_lower(col)):
self.parent.messagebox(_('The lookup name must be lower case and cannot contain ":"s or spaces'))
return
if not self.editing_col:
self.parent.custcols[col] = {
'label':col,
'name':col_heading,
'datatype':col_type,
'editable':True,
'display':None,
'normalized':None,
'num':None,
'is_multiple':is_multiple,
}
item = QListWidgetItem(col_heading, self.parent.columns)
item.setData(Qt.UserRole, QVariant(col))
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
item.setCheckState(Qt.Checked)
else:
idx = self.parent.columns.currentRow()
item = self.parent.columns.item(idx)
item.setData(Qt.UserRole, QVariant(col))
item.setText(col_heading)
self.parent.custcols[self.orig_column_name]['label'] = col
self.parent.custcols[self.orig_column_name]['name'] = col_heading
self.parent.custcols[self.orig_column_name]['*edited'] = True
self.parent.custcols[self.orig_column_name]['*must_restart'] = True
QDialog.accept(self)
def reject(self):
QDialog.reject(self)

View File

@ -0,0 +1,142 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QCreateCustomColumn</class>
<widget class="QDialog" name="QCreateCustomColumn">
<property name="windowModality">
<enum>Qt::ApplicationModal</enum>
</property>
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>391</width>
<height>157</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Create Tag-based Column</string>
</property>
<widget class="QWidget" name="verticalLayoutWidget">
<property name="geometry">
<rect>
<x>10</x>
<y>0</y>
<width>371</width>
<height>141</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0,0,0">
<property name="sizeConstraint">
<enum>QLayout::SetDefaultConstraint</enum>
</property>
<property name="margin">
<number>5</number>
</property>
<item row="2" column="0">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Lookup name</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Column heading</string>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QLineEdit" name="column_name_box">
<property name="minimumSize">
<size>
<width>20</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Used for searching the column. Must be lower case and not contain spaces or colons.</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QLineEdit" name="column_heading_box">
<property name="toolTip">
<string>Column heading in the library view and category name in tags browser</string>
</property>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Column type</string>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="column_type_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Maximum" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>70</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>What kind of information will be kept in the column.</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QDialogButtonBox" name="button_box">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_6">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Create and edit custom columns</string>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
<tabstops>
<tabstop>column_name_box</tabstop>
<tabstop>column_heading_box</tabstop>
<tabstop>column_type_box</tabstop>
<tabstop>button_box</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@ -0,0 +1,186 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from copy import copy
from PyQt4.QtCore import SIGNAL, Qt, QVariant
from PyQt4.QtGui import QDialog, QDialogButtonBox, QLineEdit, QComboBox, \
QIcon, QListWidgetItem
from PyQt4.Qt import QString
from calibre.gui2.dialogs.tag_categories_ui import Ui_TagCategories
from calibre.gui2 import qstring_to_unicode, config
from calibre.gui2 import question_dialog, error_dialog
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.constants import islinux
class Item:
def __init__(self, name, label, index, icon, exists):
self.name = name
self.label = label
self.index = index
self.icon = icon
self.exists = exists
def __str__(self):
return 'name=%s, label=%s, index=%s, exists='%(self.name, self.label, self.index, self.exists)
class TagCategories(QDialog, Ui_TagCategories):
category_labels = ['', 'author', 'series', 'publisher', 'tag']
def __init__(self, window, db, index=None):
QDialog.__init__(self, window)
Ui_TagCategories.__init__(self)
self.setupUi(self)
self.db = db
self.index = index
self.applied_items = []
category_icons = [None, QIcon(I('user_profile.svg')), QIcon(I('series.svg')),
QIcon(I('publisher.png')), QIcon(I('tags.svg'))]
category_values = [None,
lambda: [n for (id, n) in self.db.all_authors()],
lambda: [n for (id, n) in self.db.all_series()],
lambda: [n for (id, n) in self.db.all_publishers()],
lambda: self.db.all_tags()
]
category_names = ['', _('Authors'), _('Series'), _('Publishers'), _('Tags')]
self.all_items = []
self.all_items_dict = {}
for idx,label in enumerate(self.category_labels):
if idx == 0:
continue
for n in category_values[idx]():
t = Item(name=n, label=label, index=len(self.all_items),icon=category_icons[idx], exists=True)
self.all_items.append(t)
self.all_items_dict[label+':'+n] = t
self.categories = dict.copy(config['user_categories'])
if self.categories is None:
self.categories = {}
for cat in self.categories:
for item,l in enumerate(self.categories[cat]):
key = ':'.join([l[1], l[0]])
t = self.all_items_dict.get(key, None)
if t is None:
t = Item(name=l[0], label=l[1], index=len(self.all_items),
icon=category_icons[self.category_labels.index(l[1])], exists=False)
self.all_items.append(t)
self.all_items_dict[key] = t
l[2] = t.index
self.all_items_sorted = sorted(self.all_items, cmp=lambda x,y: cmp(x.name.lower(), y.name.lower()))
self.display_filtered_categories(0)
for v in category_names:
self.category_filter_box.addItem(v)
self.current_cat_name = None
self.connect(self.apply_button, SIGNAL('clicked()'), self.apply_tags)
self.connect(self.unapply_button, SIGNAL('clicked()'), self.unapply_tags)
self.connect(self.add_category_button, SIGNAL('clicked()'), self.add_category)
self.connect(self.category_box, SIGNAL('currentIndexChanged(int)'), self.select_category)
self.connect(self.category_filter_box, SIGNAL('currentIndexChanged(int)'), self.display_filtered_categories)
self.connect(self.delete_category_button, SIGNAL('clicked()'), self.del_category)
if islinux:
self.available_items_box.itemDoubleClicked.connect(self.apply_tags)
else:
self.connect(self.available_items_box, SIGNAL('itemActivated(QListWidgetItem*)'), self.apply_tags)
self.connect(self.applied_items_box, SIGNAL('itemActivated(QListWidgetItem*)'), self.unapply_tags)
self.populate_category_list()
return
self.select_category(0)
def make_list_widget(self, item):
n = item.name if item.exists else item.name + _(' (not on any book)')
w = QListWidgetItem(item.icon, n)
w.setData(Qt.UserRole, item.index)
return w
def display_filtered_categories(self, idx):
idx = idx if idx is not None else self.category_filter_box.currentIndex()
self.available_items_box.clear()
self.applied_items_box.clear()
for item in self.all_items_sorted:
if idx == 0 or item.label == self.category_labels[idx]:
if item.index not in self.applied_items and item.exists:
self.available_items_box.addItem(self.make_list_widget(item))
for index in self.applied_items:
self.applied_items_box.addItem(self.make_list_widget(self.all_items[index]))
def apply_tags(self, node=None):
if self.current_cat_name is None:
return
nodes = self.available_items_box.selectedItems() if node is None else [node]
for node in nodes:
index = self.all_items[node.data(Qt.UserRole).toPyObject()].index
if index not in self.applied_items:
self.applied_items.append(index)
self.applied_items.sort(cmp=lambda x, y:cmp(self.all_items[x].name.lower(), self.all_items[y].name.lower()))
self.display_filtered_categories(None)
def unapply_tags(self, node=None):
nodes = self.applied_items_box.selectedItems() if node is None else [node]
for node in nodes:
index = self.all_items[node.data(Qt.UserRole).toPyObject()].index
self.applied_items.remove(index)
self.display_filtered_categories(None)
def add_category(self):
self.save_category()
cat_name = qstring_to_unicode(self.input_box.text()).strip()
if cat_name == '':
return False
if cat_name not in self.categories:
self.category_box.clear()
self.current_cat_name = cat_name
self.categories[cat_name] = []
self.applied_items = []
self.populate_category_list()
self.category_box.setCurrentIndex(self.category_box.findText(cat_name))
else:
self.select_category(self.category_box.findText(cat_name))
return True
def del_category(self):
if self.current_cat_name is not None:
if not confirm('<p>'+_('The current tag category will be '
'<b>permanently deleted</b>. Are you sure?')
+'</p>', 'tag_category_delete', self):
return
del self.categories[self.current_cat_name]
self.current_cat_name = None
self.category_box.removeItem(self.category_box.currentIndex())
def select_category(self, idx):
self.save_category()
s = self.category_box.itemText(idx)
if s:
self.current_cat_name = unicode(s)
else:
self.current_cat_name = None
if self.current_cat_name:
self.applied_items = [cat[2] for cat in self.categories.get(self.current_cat_name, [])]
else:
self.applied_items = []
self.display_filtered_categories(None)
def accept(self):
self.save_category()
config['user_categories'] = self.categories
QDialog.accept(self)
def save_category(self):
if self.current_cat_name is not None:
l = []
for index in self.applied_items:
item = self.all_items[index]
l.append([item.name, item.label, item.index])
self.categories[self.current_cat_name] = l
def populate_category_list(self):
for n in sorted(self.categories.keys(), cmp=lambda x,y: cmp(x.lower(), y.lower())):
self.category_box.addItem(n)

View File

@ -0,0 +1,385 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>TagCategories</class>
<widget class="QDialog" name="TagCategories">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>588</width>
<height>482</height>
</rect>
</property>
<property name="windowTitle">
<string>Tag Editor</string>
</property>
<property name="windowIcon">
<iconset>
<normaloff>:/images/chapters.svg</normaloff>:/images/chapters.svg</iconset>
</property>
<layout class="QGridLayout">
<item row="1" column="0">
<layout class="QVBoxLayout">
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>A&amp;vailable items</string>
</property>
<property name="buddy">
<cstring>available_items_box</cstring>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QListWidget" name="available_items_box">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item row="1" column="1">
<layout class="QVBoxLayout">
<item>
<spacer>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="apply_button">
<property name="toolTip">
<string>Apply tags to current tag category</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/forward.svg</normaloff>:/images/forward.svg</iconset>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="1" column="2">
<layout class="QVBoxLayout">
<item>
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>A&amp;pplied items</string>
</property>
<property name="buddy">
<cstring>applied_items_box</cstring>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item>
<widget class="QListWidget" name="applied_items_box">
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::MultiSelection</enum>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="3">
<layout class="QVBoxLayout">
<item>
<spacer>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="unapply_button">
<property name="toolTip">
<string>Unapply (remove) tag from current tag category</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/list_remove.svg</normaloff>:/images/list_remove.svg</iconset>
</property>
</widget>
</item>
<item>
<spacer>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
<item row="3" column="0" colspan="4">
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
<item row="0" column="0" colspan="4">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label_3">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>100</width>
<height>0</height>
</size>
</property>
<property name="text">
<string>Category name: </string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>category_box</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="category_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>160</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>145</width>
<height>0</height>
</size>
</property>
<property name="toolTip">
<string>Select a category to edit</string>
</property>
<property name="editable">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="2">
<widget class="QToolButton" name="delete_category_button">
<property name="toolTip">
<string>Delete this selected tag category</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/minus.svg</normaloff>:/images/minus.svg</iconset>
</property>
</widget>
</item>
<item row="0" column="3">
<spacer>
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="4">
<widget class="QLineEdit" name="input_box">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>60</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Enter a new category name. Select the kind before adding it.</string>
</property>
</widget>
</item>
<item row="0" column="5">
<widget class="QToolButton" name="add_category_button">
<property name="toolTip">
<string>Add the new category</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset>
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>
</property>
</widget>
</item>
<item row="1" column="5">
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Category filter: </string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="category_filter_box">
<property name="toolTip">
<string>Select the content kind of the new category</string>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>
<include location="../../../../../calibre_datesearch/resources/images"/>
</resources>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>TagCategories</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>248</x>
<y>254</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>TagCategories</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>316</x>
<y>260</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -1,32 +1,34 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, textwrap, traceback, re, shutil import os, textwrap, traceback, re, shutil, functools
from operator import attrgetter from operator import attrgetter
from math import cos, sin, pi from math import cos, sin, pi
from contextlib import closing from contextlib import closing
from datetime import date
from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \ from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \
QPainterPath, QLinearGradient, QBrush, \ QPainterPath, QLinearGradient, QBrush, \
QPen, QStyle, QPainter, QStyleOptionViewItemV4, \ QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
QImage, QMenu, \ QIcon, QImage, QMenu, \
QStyledItemDelegate, QCompleter QStyledItemDelegate, QCompleter, QIntValidator, \
QPlainTextEdit, QDoubleValidator, QCheckBox, QMessageBox
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, pyqtSignal, \ from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, pyqtSignal, \
SIGNAL, QObject, QSize, QModelIndex, QDate SIGNAL, QObject, QSize, QModelIndex, QDate, QRect
from calibre import strftime from calibre import strftime
from calibre.ptempfile import PersistentTemporaryFile from calibre.ebooks.metadata import string_to_authors, fmt_sidx, authors_to_string
from calibre.utils.pyparsing import ParseException
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, \
error_dialog
from calibre.gui2.widgets import EnLineEdit, TagsLineEdit
from calibre.utils.search_query_parser import SearchQueryParser
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
from calibre.ebooks.metadata import string_to_authors, fmt_sidx, \ from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, error_dialog
authors_to_string from calibre.gui2.dialogs.comments_dialog import CommentsDialog
from calibre.utils.config import tweaks from calibre.gui2.widgets import EnLineEdit, TagsLineEdit
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import tweaks, prefs
from calibre.utils.date import dt_factory, qt_to_dt, isoformat from calibre.utils.date import dt_factory, qt_to_dt, isoformat
from calibre.utils.pyparsing import ParseException
from calibre.utils.search_query_parser import SearchQueryParser
class LibraryDelegate(QStyledItemDelegate): class LibraryDelegate(QStyledItemDelegate):
COLOR = QColor("blue") COLOR = QColor("blue")
@ -98,7 +100,6 @@ class LibraryDelegate(QStyledItemDelegate):
return sb return sb
class DateDelegate(QStyledItemDelegate): class DateDelegate(QStyledItemDelegate):
def displayText(self, val, locale): def displayText(self, val, locale):
d = val.toDate() d = val.toDate()
return d.toString('dd MMM yyyy') return d.toString('dd MMM yyyy')
@ -114,7 +115,6 @@ class DateDelegate(QStyledItemDelegate):
return qde return qde
class PubDateDelegate(QStyledItemDelegate): class PubDateDelegate(QStyledItemDelegate):
def displayText(self, val, locale): def displayText(self, val, locale):
return val.toDate().toString('MMM yyyy') return val.toDate().toString('MMM yyyy')
@ -126,7 +126,6 @@ class PubDateDelegate(QStyledItemDelegate):
return qde return qde
class TextDelegate(QStyledItemDelegate): class TextDelegate(QStyledItemDelegate):
def __init__(self, parent): def __init__(self, parent):
''' '''
Delegate for text data. If auto_complete_function needs to return a list Delegate for text data. If auto_complete_function needs to return a list
@ -150,7 +149,6 @@ class TextDelegate(QStyledItemDelegate):
return editor return editor
class TagsDelegate(QStyledItemDelegate): class TagsDelegate(QStyledItemDelegate):
def __init__(self, parent): def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent) QStyledItemDelegate.__init__(self, parent)
self.db = None self.db = None
@ -160,17 +158,101 @@ class TagsDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index): def createEditor(self, parent, option, index):
if self.db: if self.db:
col = index.model().column_map[index.column()]
if not index.model().is_custom_column(col):
editor = TagsLineEdit(parent, self.db.all_tags()) editor = TagsLineEdit(parent, self.db.all_tags())
else:
editor = TagsLineEdit(parent, sorted(list(self.db.all_custom(label=col))))
return editor;
else: else:
editor = EnLineEdit(parent) editor = EnLineEdit(parent)
return editor return editor
class CcTextDelegate(QStyledItemDelegate):
def __init__(self, parent):
'''
Delegate for text/int/float data.
'''
QStyledItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
m = index.model()
col = m.column_map[index.column()]
typ = m.custom_columns[col]['datatype']
editor = EnLineEdit(parent)
if typ == 'int':
editor.setValidator(QIntValidator(parent))
elif typ == 'float':
editor.setValidator(QDoubleValidator(parent))
else:
complete_items = sorted(list(m.db.all_custom(label=col)))
completer = QCompleter(complete_items, self)
completer.setCaseSensitivity(Qt.CaseInsensitive)
completer.setCompletionMode(QCompleter.PopupCompletion)
editor.setCompleter(completer)
return editor
class CcCommentsDelegate(QStyledItemDelegate):
def __init__(self, parent):
'''
Delegate for comments data.
'''
QStyledItemDelegate.__init__(self, parent)
self.parent = parent
def createEditor(self, parent, option, index):
m = index.model()
col = m.column_map[index.column()]
# db col is not named for the field, but for the table number. To get it,
# gui column -> column label -> table number -> db column
text = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[col]['num']]]
editor = CommentsDialog(parent, text)
d = editor.exec_()
if d:
m.setData(index, QVariant(editor.textbox.toPlainText()), Qt.EditRole)
return None
def setModelData(self, editor, model, index):
model.setData(index, QVariant(editor.textbox.text()), Qt.EditRole)
class CcBoolDelegate(QStyledItemDelegate):
def __init__(self, parent):
'''
Delegate for custom_column bool data.
'''
QStyledItemDelegate.__init__(self, parent)
def createEditor(self, parent, option, index):
m = index.model()
col = m.column_map[index.column()]
editor = QCheckBox(parent)
val = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[col]['num']]]
if tweaks['bool_custom_columns_are_tristate'] == 'no':
pass
else:
if tweaks['bool_custom_columns_are_tristate'] == 'yes':
editor.setTristate(True)
return editor
def setModelData(self, editor, model, index):
model.setData(index, QVariant(editor.checkState()), Qt.EditRole)
def setEditorData(self, editor, index):
m = index.model()
# db col is not named for the field, but for the table number. To get it,
# gui column -> column label -> table number -> db column
val = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[m.column_map[index.column()]]['num']]]
if tweaks['bool_custom_columns_are_tristate'] == 'no':
val = Qt.Unchecked if val is None or not val else Qt.Checked
else:
val = Qt.PartiallyChecked if val is None else Qt.Unchecked if not val else Qt.Checked
editor.setCheckState(val)
class BooksModel(QAbstractTableModel): class BooksModel(QAbstractTableModel):
about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted') about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted')
sorting_done = pyqtSignal(object, name='sortingDone') sorting_done = pyqtSignal(object, name='sortingDone')
headers = { orig_headers = {
'title' : _("Title"), 'title' : _("Title"),
'authors' : _("Author(s)"), 'authors' : _("Author(s)"),
'size' : _("Size (MB)"), 'size' : _("Size (MB)"),
@ -185,15 +267,22 @@ class BooksModel(QAbstractTableModel):
def __init__(self, parent=None, buffer=40): def __init__(self, parent=None, buffer=40):
QAbstractTableModel.__init__(self, parent) QAbstractTableModel.__init__(self, parent)
self.db = None self.db = None
self.column_map = config['column_map']
self.editable_cols = ['title', 'authors', 'rating', 'publisher', self.editable_cols = ['title', 'authors', 'rating', 'publisher',
'tags', 'series', 'timestamp', 'pubdate'] 'tags', 'series', 'timestamp', 'pubdate']
self.default_image = QImage(I('book.svg')) self.default_image = QImage(I('book.svg'))
self.sorted_on = ('timestamp', Qt.AscendingOrder) self.sorted_on = ('timestamp', Qt.AscendingOrder)
self.sort_history = [self.sorted_on]
self.last_search = '' # The last search performed on this model self.last_search = '' # The last search performed on this model
self.read_config() self.column_map = []
self.headers = {}
self.buffer_size = buffer self.buffer_size = buffer
self.cover_cache = None self.cover_cache = None
self.bool_yes_icon = QIcon(I('ok.svg'))
self.bool_no_icon = QIcon(I('list_remove.svg'))
self.bool_blank_icon = QIcon(I('blank.svg'))
def is_custom_column(self, cc_label):
return cc_label in self.custom_columns
def clear_caches(self): def clear_caches(self):
if self.cover_cache: if self.cover_cache:
@ -201,15 +290,24 @@ class BooksModel(QAbstractTableModel):
def read_config(self): def read_config(self):
self.use_roman_numbers = config['use_roman_numerals_for_series_number'] self.use_roman_numbers = config['use_roman_numerals_for_series_number']
cols = config['column_map'] self.column_map = config['column_map'][:] # force a copy
if cols != self.column_map: self.headers = {}
self.column_map = cols for i in self.column_map: # take out any columns no longer in the db
if not i in self.orig_headers and not i in self.custom_columns:
self.column_map.remove(i)
for i in self.column_map:
if i in self.orig_headers:
self.headers[i] = self.orig_headers[i]
elif i in self.custom_columns:
self.headers[i] = self.custom_columns[i]['name']
self.build_data_convertors()
self.reset() self.reset()
self.emit(SIGNAL('columns_sorted()')) self.emit(SIGNAL('columns_sorted()'))
def set_database(self, db): def set_database(self, db):
self.db = db self.db = db
self.build_data_convertors() self.custom_columns = self.db.custom_column_label_map
self.read_config()
def refresh_ids(self, ids, current_row=-1): def refresh_ids(self, ids, current_row=-1):
rows = self.db.refresh_ids(ids) rows = self.db.refresh_ids(ids)
@ -316,6 +414,8 @@ class BooksModel(QAbstractTableModel):
self.clear_caches() self.clear_caches()
self.reset() self.reset()
self.sorted_on = (self.column_map[col], order) self.sorted_on = (self.column_map[col], order)
self.sort_history.insert(0, self.sorted_on)
del self.sort_history[3:] # clean up older searches
self.sorting_done.emit(self.db.index) self.sorting_done.emit(self.db.index)
def refresh(self, reset=True): def refresh(self, reset=True):
@ -323,10 +423,8 @@ class BooksModel(QAbstractTableModel):
col = self.column_map.index(self.sorted_on[0]) col = self.column_map.index(self.sorted_on[0])
except: except:
col = 0 col = 0
self.db.refresh(field=self.column_map[col], self.db.refresh(field=None)
ascending=self.sorted_on[1]==Qt.AscendingOrder) self.sort(col, self.sorted_on[1], reset=reset)
if reset:
self.reset()
def resort(self, reset=True): def resort(self, reset=True):
try: try:
@ -430,6 +528,7 @@ class BooksModel(QAbstractTableModel):
return ans return ans
def get_metadata(self, rows, rows_are_ids=False, full_metadata=False): def get_metadata(self, rows, rows_are_ids=False, full_metadata=False):
# Should this add the custom columns? It doesn't at the moment
metadata, _full_metadata = [], [] metadata, _full_metadata = [], []
if not rows_are_ids: if not rows_are_ids:
rows = [self.db.id(row.row()) for row in rows] rows = [self.db.id(row.row()) for row in rows]
@ -562,75 +661,119 @@ class BooksModel(QAbstractTableModel):
return img return img
def build_data_convertors(self): def build_data_convertors(self):
def authors(r, idx=-1):
tidx = self.db.FIELD_MAP['title'] au = self.db.data[r][idx]
aidx = self.db.FIELD_MAP['authors']
sidx = self.db.FIELD_MAP['size']
ridx = self.db.FIELD_MAP['rating']
pidx = self.db.FIELD_MAP['publisher']
tmdx = self.db.FIELD_MAP['timestamp']
pddx = self.db.FIELD_MAP['pubdate']
srdx = self.db.FIELD_MAP['series']
tgdx = self.db.FIELD_MAP['tags']
siix = self.db.FIELD_MAP['series_index']
def authors(r):
au = self.db.data[r][aidx]
if au: if au:
au = [a.strip().replace('|', ',') for a in au.split(',')] au = [a.strip().replace('|', ',') for a in au.split(',')]
return ' & '.join(au) return QVariant(' & '.join(au))
else:
return None
def timestamp(r): def tags(r, idx=-1):
dt = self.db.data[r][tmdx] tags = self.db.data[r][idx]
if dt:
return QDate(dt.year, dt.month, dt.day)
def pubdate(r):
dt = self.db.data[r][pddx]
if dt:
return QDate(dt.year, dt.month, dt.day)
def rating(r):
r = self.db.data[r][ridx]
r = r/2 if r else 0
return r
def publisher(r):
pub = self.db.data[r][pidx]
if pub:
return pub
def tags(r):
tags = self.db.data[r][tgdx]
if tags: if tags:
return ', '.join(sorted(tags.split(','))) return QVariant(', '.join(sorted(tags.split(','))))
return None
def series(r): def series(r, idx=-1, siix=-1):
series = self.db.data[r][srdx] series = self.db.data[r][idx]
if series: if series:
idx = fmt_sidx(self.db.data[r][siix]) idx = fmt_sidx(self.db.data[r][siix])
return series + ' [%s]'%idx return QVariant(series + ' [%s]'%idx)
def size(r): return None
size = self.db.data[r][sidx]
def size(r, idx=-1):
size = self.db.data[r][idx]
if size: if size:
return '%.1f'%(float(size)/(1024*1024)) return QVariant('%.1f'%(float(size)/(1024*1024)))
return None
def rating_type(r, idx=-1):
r = self.db.data[r][idx]
r = r/2 if r else 0
return QVariant(r)
def datetime_type(r, idx=-1):
val = self.db.data[r][idx]
if val is not None:
return QVariant(QDate(val))
else:
return QVariant(QDate())
def bool_type(r, idx=-1):
return None # displayed using a decorator
def bool_type_decorator(r, idx=-1, bool_cols_are_tristate=True):
val = self.db.data[r][idx]
if not bool_cols_are_tristate:
if val is None or not val:
return self.bool_no_icon
if val:
return self.bool_yes_icon
if val is None:
return self.bool_blank_icon
return self.bool_no_icon
def text_type(r, mult=False, idx=-1):
text = self.db.data[r][idx]
if text and mult:
return QVariant(', '.join(sorted(text.split('|'))))
return QVariant(text)
def number_type(r, idx=-1):
return QVariant(self.db.data[r][idx])
self.dc = { self.dc = {
'title' : lambda r : self.db.data[r][tidx], 'title' : functools.partial(text_type, idx=self.db.FIELD_MAP['title'], mult=False),
'authors' : authors, 'authors' : functools.partial(authors, idx=self.db.FIELD_MAP['authors']),
'size' : size, 'size' : functools.partial(size, idx=self.db.FIELD_MAP['size']),
'timestamp': timestamp, 'timestamp': functools.partial(datetime_type, idx=self.db.FIELD_MAP['timestamp']),
'pubdate' : pubdate, 'pubdate' : functools.partial(datetime_type, idx=self.db.FIELD_MAP['pubdate']),
'rating' : rating, 'rating' : functools.partial(rating_type, idx=self.db.FIELD_MAP['rating']),
'publisher': publisher, 'publisher': functools.partial(text_type, idx=self.db.FIELD_MAP['title'], mult=False),
'tags' : tags, 'tags' : functools.partial(tags, idx=self.db.FIELD_MAP['tags']),
'series' : series, 'series' : functools.partial(series, idx=self.db.FIELD_MAP['series'], siix=self.db.FIELD_MAP['series_index']),
} }
self.dc_decorator = {}
# Add the custom columns to the data converters
for col in self.custom_columns:
idx = self.db.FIELD_MAP[self.custom_columns[col]['num']]
datatype = self.custom_columns[col]['datatype']
if datatype in ('text', 'comments'):
self.dc[col] = functools.partial(text_type, idx=idx, mult=self.custom_columns[col]['is_multiple'])
elif datatype in ('int', 'float'):
self.dc[col] = functools.partial(number_type, idx=idx)
elif datatype == 'datetime':
self.dc[col] = functools.partial(datetime_type, idx=idx)
elif datatype == 'bool':
self.dc[col] = functools.partial(bool_type, idx=idx)
self.dc_decorator[col] = functools.partial(
bool_type_decorator, idx=idx,
bool_cols_are_tristate=tweaks['bool_custom_columns_are_tristate'] == 'yes')
elif datatype == 'rating':
self.dc[col] = functools.partial(rating_type, idx=idx)
else:
print 'What type is this?', col, datatype
# build a index column to data converter map, to remove the string lookup in the data loop
self.column_to_dc_map = []
self.column_to_dc_decorator_map = []
for col in self.column_map:
self.column_to_dc_map.append(self.dc[col])
self.column_to_dc_decorator_map.append(self.dc_decorator.get(col, None))
def data(self, index, role): def data(self, index, role):
col = index.column()
# in obscure cases where custom columns are both edited and added, for a time
# the column map does not accurately represent the screen. In these cases,
# we will get asked to display columns we don't know about. Must test for this.
if col >= len(self.column_to_dc_map):
return None
if role in (Qt.DisplayRole, Qt.EditRole): if role in (Qt.DisplayRole, Qt.EditRole):
ans = self.dc[self.column_map[index.column()]](index.row()) return self.column_to_dc_map[col](index.row())
return NONE if ans is None else QVariant(ans) elif role == Qt.DecorationRole:
if self.column_to_dc_decorator_map[col] is not None:
return self.column_to_dc_decorator_map[index.column()](index.row())
#elif role == Qt.TextAlignmentRole and self.column_map[index.column()] in ('size', 'timestamp'): #elif role == Qt.TextAlignmentRole and self.column_map[index.column()] in ('size', 'timestamp'):
# return QVariant(Qt.AlignVCenter | Qt.AlignCenter) # return QVariant(Qt.AlignVCenter | Qt.AlignCenter)
#elif role == Qt.ToolTipRole and index.isValid(): #elif role == Qt.ToolTipRole and index.isValid():
@ -639,24 +782,62 @@ class BooksModel(QAbstractTableModel):
return NONE return NONE
def headerData(self, section, orientation, role): def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
if orientation == Qt.Horizontal: if orientation == Qt.Horizontal:
if section >= len(self.column_map): # same problem as in data, the column_map can be wrong
return None
if role == Qt.ToolTipRole:
return QVariant(_('The lookup/search name is "{0}"').format(self.column_map[section]))
if role == Qt.DisplayRole:
return QVariant(self.headers[self.column_map[section]]) return QVariant(self.headers[self.column_map[section]])
else: return NONE
if role == Qt.DisplayRole: # orientation is vertical
return QVariant(section+1) return QVariant(section+1)
return NONE
def flags(self, index): def flags(self, index):
flags = QAbstractTableModel.flags(self, index) flags = QAbstractTableModel.flags(self, index)
if index.isValid(): if index.isValid():
if self.column_map[index.column()] in self.editable_cols: colhead = self.column_map[index.column()]
if colhead in self.editable_cols:
flags |= Qt.ItemIsEditable
elif self.is_custom_column(colhead):
if self.custom_columns[colhead]['editable']:
flags |= Qt.ItemIsEditable flags |= Qt.ItemIsEditable
return flags return flags
def set_custom_column_data(self, row, colhead, value):
typ = self.custom_columns[colhead]['datatype']
if typ in ('text', 'comments'):
val = qstring_to_unicode(value.toString()).strip()
val = val if val else None
if typ == 'bool':
val = value.toInt()[0] # tristate checkboxes put unknown in the middle
val = None if val == 1 else False if val == 0 else True
elif typ == 'rating':
val = value.toInt()[0]
val = 0 if val < 0 else 5 if val > 5 else val
val *= 2
elif typ in ('int', 'float'):
val = qstring_to_unicode(value.toString()).strip()
if val is None or not val:
val = None
elif typ == 'datetime':
val = value.toDate()
if val.isNull() or not val.isValid():
return False
val = qt_to_dt(val, as_utc=False)
self.db.set_custom(self.db.id(row), val, label=colhead, num=None, append=False, notify=True)
return True
def setData(self, index, value, role): def setData(self, index, value, role):
if role == Qt.EditRole: if role == Qt.EditRole:
row, col = index.row(), index.column() row, col = index.row(), index.column()
column = self.column_map[col] column = self.column_map[col]
if self.is_custom_column(column):
if not self.set_custom_column_data(row, column, value):
return False
else:
if column not in self.editable_cols: if column not in self.editable_cols:
return False return False
val = int(value.toInt()[0]) if column == 'rating' else \ val = int(value.toInt()[0]) if column == 'rating' else \
@ -695,9 +876,11 @@ class BooksModel(QAbstractTableModel):
index, index) index, index)
if column == self.sorted_on[0]: if column == self.sorted_on[0]:
self.resort() self.resort()
return True return True
def set_search_restriction(self, s):
self.db.data.set_search_restriction(s)
class BooksView(TableView): class BooksView(TableView):
TIME_FMT = '%d %b %Y' TIME_FMT = '%d %b %Y'
wrapper = textwrap.TextWrapper(width=20) wrapper = textwrap.TextWrapper(width=20)
@ -721,6 +904,9 @@ class BooksView(TableView):
self.authors_delegate = TextDelegate(self) self.authors_delegate = TextDelegate(self)
self.series_delegate = TextDelegate(self) self.series_delegate = TextDelegate(self)
self.publisher_delegate = TextDelegate(self) self.publisher_delegate = TextDelegate(self)
self.cc_text_delegate = CcTextDelegate(self)
self.cc_bool_delegate = CcBoolDelegate(self)
self.cc_comments_delegate = CcCommentsDelegate(self)
self.display_parent = parent self.display_parent = parent
self._model = modelcls(self) self._model = modelcls(self)
self.setModel(self._model) self.setModel(self._model)
@ -775,6 +961,25 @@ class BooksView(TableView):
self.setItemDelegateForColumn(cm.index('publisher'), self.publisher_delegate) self.setItemDelegateForColumn(cm.index('publisher'), self.publisher_delegate)
if 'series' in cm: if 'series' in cm:
self.setItemDelegateForColumn(cm.index('series'), self.series_delegate) self.setItemDelegateForColumn(cm.index('series'), self.series_delegate)
for colhead in cm:
if not self._model.is_custom_column(colhead):
continue
cc = self._model.custom_columns[colhead]
if cc['datatype'] == 'datetime':
self.setItemDelegateForColumn(cm.index(colhead), self.timestamp_delegate)
elif cc['datatype'] == 'comments':
self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate)
elif cc['datatype'] == 'text':
if cc['is_multiple']:
self.setItemDelegateForColumn(cm.index(colhead), self.tags_delegate)
else:
self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
elif cc['datatype'] in ('int', 'float'):
self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
elif cc['datatype'] == 'bool':
self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate)
elif cc['datatype'] == 'rating':
self.setItemDelegateForColumn(cm.index(colhead), self.rating_delegate)
def set_context_menu(self, edit_metadata, send_to_device, convert, view, def set_context_menu(self, edit_metadata, send_to_device, convert, view,
save, open_folder, book_details, delete, similar_menu=None): save, open_folder, book_details, delete, similar_menu=None):
@ -801,6 +1006,16 @@ class BooksView(TableView):
self.context_menu.popup(event.globalPos()) self.context_menu.popup(event.globalPos())
event.accept() event.accept()
def restore_sort_at_startup(self, saved_history):
if tweaks['sort_columns_at_startup'] is not None:
saved_history = tweaks['sort_columns_at_startup']
if saved_history is None:
return
for col,order in reversed(saved_history):
self.sortByColumn(col, order)
self.model().sort_history = saved_history
def sortByColumn(self, colname, order): def sortByColumn(self, colname, order):
try: try:
idx = self._model.column_map.index(colname) idx = self._model.column_map.index(colname)
@ -836,7 +1051,6 @@ class BooksView(TableView):
event.accept() event.accept()
self.emit(SIGNAL('files_dropped(PyQt_PyObject)'), paths) self.emit(SIGNAL('files_dropped(PyQt_PyObject)'), paths)
def set_database(self, db): def set_database(self, db):
self._model.set_database(db) self._model.set_database(db)
self.tags_delegate.set_database(db) self.tags_delegate.set_database(db)
@ -857,6 +1071,10 @@ class BooksView(TableView):
self.connect(self._model, SIGNAL('searched(PyQt_PyObject)'), self.connect(self._model, SIGNAL('searched(PyQt_PyObject)'),
self.search_done) self.search_done)
def connect_to_restriction_set(self, tv):
QObject.connect(tv, SIGNAL('restriction_set(PyQt_PyObject)'),
self._model.set_search_restriction)
def connect_to_book_display(self, bd): def connect_to_book_display(self, bd):
QObject.connect(self._model, SIGNAL('new_bookdisplay_data(PyQt_PyObject)'), QObject.connect(self._model, SIGNAL('new_bookdisplay_data(PyQt_PyObject)'),
bd) bd)
@ -952,7 +1170,6 @@ class OnDeviceSearch(SearchQueryParser):
matches.add(index) matches.add(index)
break break
except ValueError: # Unicode errors except ValueError: # Unicode errors
import traceback
traceback.print_exc() traceback.print_exc()
return matches return matches
@ -1190,5 +1407,6 @@ class DeviceBooksModel(BooksModel):
def set_editable(self, editable): def set_editable(self, editable):
self.editable = editable self.editable = editable
def set_search_restriction(self, s):
pass

View File

@ -306,6 +306,12 @@
<layout class="QVBoxLayout" name="verticalLayout"> <layout class="QVBoxLayout" name="verticalLayout">
<item> <item>
<widget class="TagsView" name="tags_view"> <widget class="TagsView" name="tags_view">
<property name="maximumSize">
<size>
<width>256</width>
<height>16777215</height>
</size>
</property>
<property name="tabKeyNavigation"> <property name="tabKeyNavigation">
<bool>true</bool> <bool>true</bool>
</property> </property>
@ -327,6 +333,8 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item> <item>
<widget class="QComboBox" name="tag_match"> <widget class="QComboBox" name="tag_match">
<property name="currentIndex"> <property name="currentIndex">
@ -344,6 +352,42 @@
</item> </item>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="edit_categories">
<property name="text">
<string>Manage user categories</string>
</property>
<property name="toolTip">
<string>Create, edit, and delete user categories</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="10,50">
<item>
<widget class="QLabel" name="restriction_label">
<property name="text">
<string>Restrict display to:</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="search_restriction">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>50</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="toolTip">
<string>Books display will be restricted to those matching the selected saved search</string>
</property>
</widget>
</item>
</layout>
</item>
</layout> </layout>
</item> </item>
<item> <item>

View File

@ -10,6 +10,7 @@ from PyQt4.Qt import QComboBox, SIGNAL, Qt, QLineEdit, QStringList, pyqtSlot
from PyQt4.QtGui import QCompleter from PyQt4.QtGui import QCompleter
from calibre.gui2 import config from calibre.gui2 import config
from calibre.gui2.dialogs.confirm_delete import confirm
class SearchLineEdit(QLineEdit): class SearchLineEdit(QLineEdit):
@ -226,7 +227,6 @@ class SavedSearchBox(QComboBox):
self.clear_to_help() self.clear_to_help()
def normalize_state(self): def normalize_state(self):
#print 'in normalize_state'
self.setEditText('') self.setEditText('')
self.line_edit.setStyleSheet( self.line_edit.setStyleSheet(
'QLineEdit { color: black; background-color: %s; }' % 'QLineEdit { color: black; background-color: %s; }' %
@ -234,7 +234,6 @@ class SavedSearchBox(QComboBox):
self.help_state = False self.help_state = False
def clear_to_help(self): def clear_to_help(self):
#print 'in clear_to_help'
self.setToolTip(self.tool_tip_text) self.setToolTip(self.tool_tip_text)
self.initialize_saved_search_names() self.initialize_saved_search_names()
self.setEditText(self.help_text) self.setEditText(self.help_text)
@ -245,12 +244,10 @@ class SavedSearchBox(QComboBox):
self.normal_background) self.normal_background)
def focus_out(self, event): def focus_out(self, event):
#print 'in focus_out'
if self.currentText() == '': if self.currentText() == '':
self.clear_to_help() self.clear_to_help()
def key_pressed(self, event): def key_pressed(self, event):
#print 'in key_pressed'
if self.help_state: if self.help_state:
self.normalize_state() self.normalize_state()
@ -259,7 +256,6 @@ class SavedSearchBox(QComboBox):
self.normalize_state() self.normalize_state()
def saved_search_selected (self, qname): def saved_search_selected (self, qname):
#print 'in saved_search_selected'
qname = unicode(qname) qname = unicode(qname)
if qname is None or not qname.strip(): if qname is None or not qname.strip():
return return
@ -269,7 +265,6 @@ class SavedSearchBox(QComboBox):
self.setToolTip(self.saved_searches.lookup(qname)) self.setToolTip(self.saved_searches.lookup(qname))
def initialize_saved_search_names(self): def initialize_saved_search_names(self):
#print 'in initialize_saved_search_names'
self.clear() self.clear()
qnames = self.saved_searches.names() qnames = self.saved_searches.names()
self.addItems(qnames) self.addItems(qnames)
@ -277,7 +272,10 @@ class SavedSearchBox(QComboBox):
# SIGNALed from the main UI # SIGNALed from the main UI
def delete_search_button_clicked(self): def delete_search_button_clicked(self):
#print 'in delete_search_button_clicked' if not confirm('<p>'+_('The selected search will be '
'<b>permanently deleted</b>. Are you sure?')
+'</p>', 'saved_search_delete', self):
return
idx = self.currentIndex idx = self.currentIndex
if idx < 0: if idx < 0:
return return
@ -288,7 +286,6 @@ class SavedSearchBox(QComboBox):
# SIGNALed from the main UI # SIGNALed from the main UI
def save_search_button_clicked(self): def save_search_button_clicked(self):
#print 'in save_search_button_clicked'
name = unicode(self.currentText()) name = unicode(self.currentText())
if self.help_state or not name.strip(): if self.help_state or not name.strip():
name = unicode(self.search_box.text()).replace('"', '') name = unicode(self.search_box.text()).replace('"', '')
@ -305,10 +302,7 @@ class SavedSearchBox(QComboBox):
# SIGNALed from the main UI # SIGNALed from the main UI
def copy_search_button_clicked (self): def copy_search_button_clicked (self):
#print 'in copy_search_button_clicked'
idx = self.currentIndex(); idx = self.currentIndex();
if idx < 0: if idx < 0:
return return
self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText()))) self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText())))

View File

@ -8,11 +8,13 @@ Browsing book collection by tags.
''' '''
from itertools import izip from itertools import izip
from copy import copy
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \ from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
QFont, SIGNAL, QSize, QIcon, QPoint, \ QFont, SIGNAL, QSize, QIcon, QPoint, \
QAbstractItemModel, QVariant, QModelIndex QAbstractItemModel, QVariant, QModelIndex
from calibre.gui2 import config, NONE from calibre.gui2 import config, NONE
from calibre.utils.config import prefs
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.library.database2 import Tag from calibre.library.database2 import Tag
@ -27,16 +29,24 @@ class TagsView(QTreeView):
self.setIconSize(QSize(30, 30)) self.setIconSize(QSize(30, 30))
self.tag_match = None self.tag_match = None
def set_database(self, db, tag_match, popularity): def set_database(self, db, tag_match, popularity, restriction):
self._model = TagsModel(db, parent=self) self._model = TagsModel(db, parent=self)
self.popularity = popularity self.popularity = popularity
self.restriction = restriction
self.tag_match = tag_match self.tag_match = tag_match
self.db = db
self.setModel(self._model) self.setModel(self._model)
self.connect(self, SIGNAL('clicked(QModelIndex)'), self.toggle) self.connect(self, SIGNAL('clicked(QModelIndex)'), self.toggle)
self.popularity.setChecked(config['sort_by_popularity']) self.popularity.setChecked(config['sort_by_popularity'])
self.connect(self.popularity, SIGNAL('stateChanged(int)'), self.sort_changed) self.connect(self.popularity, SIGNAL('stateChanged(int)'), self.sort_changed)
self.connect(self.restriction, SIGNAL('activated(const QString&)'), self.search_restriction_set)
self.need_refresh.connect(self.recount, type=Qt.QueuedConnection) self.need_refresh.connect(self.recount, type=Qt.QueuedConnection)
db.add_listener(self.database_changed) db.add_listener(self.database_changed)
self.saved_searches_changed(recount=False)
def create_tag_category(self, name, tag_list):
self._model.create_tag_category(name, tag_list)
self.recount()
def database_changed(self, event, ids): def database_changed(self, event, ids):
self.need_refresh.emit() self.need_refresh.emit()
@ -48,6 +58,19 @@ class TagsView(QTreeView):
def sort_changed(self, state): def sort_changed(self, state):
config.set('sort_by_popularity', state == Qt.Checked) config.set('sort_by_popularity', state == Qt.Checked)
self.model().refresh() self.model().refresh()
# self.search_restriction_set()
def search_restriction_set(self, s):
self.clear()
if len(s) == 0:
self.search_restriction = ''
else:
self.search_restriction = unicode(s)
self.model().set_search_restriction(self.search_restriction)
self.recount()
self.emit(SIGNAL('restriction_set(PyQt_PyObject)'), self.search_restriction)
self.emit(SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
self._model.tokens(), self.match_all)
def toggle(self, index): def toggle(self, index):
modifiers = int(QApplication.keyboardModifiers()) modifiers = int(QApplication.keyboardModifiers())
@ -59,6 +82,20 @@ class TagsView(QTreeView):
def clear(self): def clear(self):
self.model().clear_state() self.model().clear_state()
def saved_searches_changed(self, recount=True):
p = prefs['saved_searches'].keys()
p.sort()
t = self.restriction.currentText()
self.restriction.clear() # rebuild the restrictions combobox using current saved searches
self.restriction.addItem('')
for s in p:
self.restriction.addItem(s)
if t in p: # redo the current restriction, if there was one
self.restriction.setCurrentIndex(self.restriction.findText(t))
self.search_restriction_set(t)
if recount:
self.recount()
def recount(self, *args): def recount(self, *args):
ci = self.currentIndex() ci = self.currentIndex()
if not ci.isValid(): if not ci.isValid():
@ -74,13 +111,22 @@ class TagsView(QTreeView):
self.setCurrentIndex(idx) self.setCurrentIndex(idx)
self.scrollTo(idx, QTreeView.PositionAtCenter) self.scrollTo(idx, QTreeView.PositionAtCenter)
'''
If the number of user categories changed, or if custom columns have come or gone,
we must rebuild the model. Reason: it is much easier to do that than to reconstruct
the browser tree.
'''
def set_new_model(self):
self._model = TagsModel(self.db, parent=self)
self.setModel(self._model)
class TagTreeItem(object): class TagTreeItem(object):
CATEGORY = 0 CATEGORY = 0
TAG = 1 TAG = 1
ROOT = 2 ROOT = 2
def __init__(self, data=None, tag=None, category_icon=None, icon_map=None, parent=None): def __init__(self, data=None, category_icon=None, icon_map=None, parent=None):
self.parent = parent self.parent = parent
self.children = [] self.children = []
if self.parent is not None: if self.parent is not None:
@ -96,13 +142,14 @@ class TagTreeItem(object):
self.bold_font.setBold(True) self.bold_font.setBold(True)
self.bold_font = QVariant(self.bold_font) self.bold_font = QVariant(self.bold_font)
elif self.type == self.TAG: elif self.type == self.TAG:
self.tag, self.icon_map = data, list(map(QVariant, icon_map)) icon_map[0] = data.icon
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
def __str__(self): def __str__(self):
if self.type == self.ROOT: if self.type == self.ROOT:
return 'ROOT' return 'ROOT'
if self.type == self.CATEGORY: if self.type == self.CATEGORY:
return 'CATEGORY:'+self.name+':%d'%len(self.children) return 'CATEGORY:'+str(QVariant.toString(self.name))+':%d'%len(self.children)
return 'TAG:'+self.tag.name return 'TAG:'+self.tag.name
def row(self): def row(self):
@ -137,7 +184,7 @@ class TagTreeItem(object):
else: else:
return QVariant('[%d] %s'%(self.tag.count, self.tag.name)) return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
if role == Qt.DecorationRole: if role == Qt.DecorationRole:
return self.icon_map[self.tag.state] return self.icon_state_map[self.tag.state]
if role == Qt.ToolTipRole and self.tag.tooltip: if role == Qt.ToolTipRole and self.tag.tooltip:
return QVariant(self.tag.tooltip) return QVariant(self.tag.tooltip)
return NONE return NONE
@ -148,38 +195,100 @@ class TagTreeItem(object):
class TagsModel(QAbstractItemModel): class TagsModel(QAbstractItemModel):
categories = [_('Authors'), _('Series'), _('Formats'), _('Publishers'), _('News'), _('Tags'), _('Searches')] categories_orig = [_('Authors'), _('Series'), _('Formats'), _('Publishers'), _('News'), _('All tags')]
row_map = ['author', 'series', 'format', 'publisher', 'news', 'tag', 'search'] row_map_orig = ['author', 'series', 'format', 'publisher', 'news', 'tag']
tags_categories_start= 5
search_keys=['search', _('Searches')]
def __init__(self, db, parent=None): def __init__(self, db, parent=None):
QAbstractItemModel.__init__(self, parent) QAbstractItemModel.__init__(self, parent)
self.cmap = tuple(map(QIcon, [I('user_profile.svg'), self.cat_icon_map_orig = list(map(QIcon, [I('user_profile.svg'),
I('series.svg'), I('book.svg'), I('publisher.png'), I('series.svg'), I('book.svg'), I('publisher.png'),
I('news.svg'), I('tags.svg'), I('search.svg')])) I('news.svg'), I('tags.svg')]))
self.icon_map = [QIcon(), QIcon(I('plus.svg')), self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
QIcon(I('minus.svg'))] self.custcol_icon = QIcon(I('column.svg'))
self.search_icon = QIcon(I('search.svg'))
self.usercat_icon = QIcon(I('drawer.svg'))
self.label_to_icon_map = dict(map(None, self.row_map_orig, self.cat_icon_map_orig))
self.label_to_icon_map['*custom'] = self.custcol_icon
self.db = db self.db = db
self.search_restriction = ''
self.user_categories = {}
self.ignore_next_search = 0 self.ignore_next_search = 0
data = self.get_node_tree(config['sort_by_popularity'])
self.root_item = TagTreeItem() self.root_item = TagTreeItem()
data = self.db.get_categories(config['sort_by_popularity'])
data['search'] = self.get_search_nodes()
for i, r in enumerate(self.row_map): for i, r in enumerate(self.row_map):
c = TagTreeItem(parent=self.root_item, c = TagTreeItem(parent=self.root_item,
data=self.categories[i], category_icon=self.cmap[i]) data=self.categories[i], category_icon=self.cat_icon_map[i])
for tag in data[r]: for tag in data[r]:
TagTreeItem(parent=c, data=tag, icon_map=self.icon_map) TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
def set_search_restriction(self, s):
self.search_restriction = s
def get_search_nodes(self): def get_node_tree(self, sort):
self.row_map = []
self.categories = []
# strip the icons after the 'standard' categories. We will put them back later
self.cat_icon_map = self.cat_icon_map_orig[:self.tags_categories_start-len(self.row_map_orig)]
self.user_categories = dict.copy(config['user_categories'])
column_map = config['column_map']
for i in range(0, self.tags_categories_start): # First the standard categories
self.row_map.append(self.row_map_orig[i])
self.categories.append(self.categories_orig[i])
if len(self.search_restriction):
data = self.db.get_categories(sort_on_count=sort, icon_map=self.label_to_icon_map,
ids=self.db.search(self.search_restriction, return_matches=True))
else:
data = self.db.get_categories(sort_on_count=sort, icon_map=self.label_to_icon_map)
for c in data: # now the custom columns
if c not in self.row_map_orig and c in column_map:
self.row_map.append(c)
self.categories.append(self.db.custom_column_label_map[c]['name'])
self.cat_icon_map.append(self.custcol_icon)
# Now do the user-defined categories. There is a time/space tradeoff here.
# By converting the tags into a map, we can do the verification in the category
# loop much faster, at the cost of duplicating the categories lists.
taglist = {}
for c in self.row_map_orig:
taglist[c] = dict(map(lambda t:(t.name if c != 'author' else t.name.replace('|', ','), t), data[c]))
for c in self.user_categories:
l = []
for (name,label,ign) in self.user_categories[c]:
if name in taglist[label]: # use same node as the complete category
l.append(taglist[label][name])
# else: do nothing, to eliminate nodes that have zero counts
if config['sort_by_popularity']:
data[c+'*'] = sorted(l, cmp=(lambda x, y: cmp(x.count, y.count)))
else:
data[c+'*'] = sorted(l, cmp=(lambda x, y: cmp(x.name.lower(), y.name.lower())))
self.row_map.append(c+'*')
self.categories.append(c)
self.cat_icon_map.append(self.usercat_icon)
# Now the rest of the normal tag categories
for i in range(self.tags_categories_start, len(self.row_map_orig)):
self.row_map.append(self.row_map_orig[i])
self.categories.append(self.categories_orig[i])
self.cat_icon_map.append(self.cat_icon_map_orig[i])
data['search'] = self.get_search_nodes(self.search_icon) # Add the search category
self.row_map.append(self.search_keys[0])
self.categories.append(self.search_keys[1])
self.cat_icon_map.append(self.search_icon)
return data
def get_search_nodes(self, icon):
l = [] l = []
for i in saved_searches.names(): for i in saved_searches.names():
l.append(Tag(i, tooltip=saved_searches.lookup(i))) l.append(Tag(i, tooltip=saved_searches.lookup(i), icon=icon))
return l return l
def refresh(self): def refresh(self):
data = self.db.get_categories(config['sort_by_popularity']) data = self.get_node_tree(config['sort_by_popularity']) # get category data
data['search'] = self.get_search_nodes()
for i, r in enumerate(self.row_map): for i, r in enumerate(self.row_map):
category = self.root_item.children[i] category = self.root_item.children[i]
names = [t.tag.name for t in category.children] names = [t.tag.name for t in category.children]
@ -194,10 +303,8 @@ class TagsModel(QAbstractItemModel):
if len(data[r]) > 0: if len(data[r]) > 0:
self.beginInsertRows(category_index, 0, len(data[r])-1) self.beginInsertRows(category_index, 0, len(data[r])-1)
for tag in data[r]: for tag in data[r]:
if r == 'author':
tag.name = tag.name.replace('|', ',')
tag.state = state_map.get(tag.name, 0) tag.state = state_map.get(tag.name, 0)
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_map) t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
self.endInsertRows() self.endInsertRows()
def columnCount(self, parent): def columnCount(self, parent):
@ -273,16 +380,20 @@ class TagsModel(QAbstractItemModel):
return len(parent_item.children) return len(parent_item.children)
def reset_all_states(self, except_=None): def reset_all_states(self, except_=None):
update_list = []
for i in xrange(self.rowCount(QModelIndex())): for i in xrange(self.rowCount(QModelIndex())):
category_index = self.index(i, 0, QModelIndex()) category_index = self.index(i, 0, QModelIndex())
for j in xrange(self.rowCount(category_index)): for j in xrange(self.rowCount(category_index)):
tag_index = self.index(j, 0, category_index) tag_index = self.index(j, 0, category_index)
tag_item = tag_index.internalPointer() tag_item = tag_index.internalPointer()
if tag_item is except_:
continue
tag = tag_item.tag tag = tag_item.tag
if tag.state != 0: if tag is except_:
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
tag_index, tag_index)
continue
if tag.state != 0 or tag in update_list:
tag.state = 0 tag.state = 0
update_list.append(tag)
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
tag_index, tag_index) tag_index, tag_index)
@ -299,9 +410,9 @@ class TagsModel(QAbstractItemModel):
if not index.isValid(): return False if not index.isValid(): return False
item = index.internalPointer() item = index.internalPointer()
if item.type == TagTreeItem.TAG: if item.type == TagTreeItem.TAG:
if exclusive:
self.reset_all_states(except_=item)
item.toggle() item.toggle()
if exclusive:
self.reset_all_states(except_=item.tag)
self.ignore_next_search = 2 self.ignore_next_search = 2
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), index, index) self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), index, index)
return True return True
@ -309,14 +420,19 @@ class TagsModel(QAbstractItemModel):
def tokens(self): def tokens(self):
ans = [] ans = []
tags_seen = []
for i, key in enumerate(self.row_map): for i, key in enumerate(self.row_map):
if key.endswith('*'): # User category, so skip it. The tag will be marked in its real category
continue
category_item = self.root_item.children[i] category_item = self.root_item.children[i]
for tag_item in category_item.children: for tag_item in category_item.children:
tag = tag_item.tag tag = tag_item.tag
category = key if key != 'news' else 'tag'
if tag.state > 0: if tag.state > 0:
prefix = ' not ' if tag.state == 2 else '' prefix = ' not ' if tag.state == 2 else ''
category = key if key != 'news' else 'tag'
if category == 'tag':
if tag.name in tags_seen:
continue
tags_seen.append(tag.name)
ans.append('%s%s:"=%s"'%(prefix, category, tag.name)) ans.append('%s%s:"=%s"'%(prefix, category, tag.name))
return ans return ans

View File

@ -60,6 +60,9 @@ from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
from calibre.library.database2 import LibraryDatabase2 from calibre.library.database2 import LibraryDatabase2
from calibre.library.caches import CoverCache from calibre.library.caches import CoverCache
from calibre.gui2.dialogs.confirm_delete import confirm from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.tag_categories import TagCategories
from datetime import datetime
class SaveMenu(QMenu): class SaveMenu(QMenu):
@ -126,8 +129,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
pixmap_to_data(pixmap)) pixmap_to_data(pixmap))
def __init__(self, listener, opts, actions, parent=None): def __init__(self, listener, opts, actions, parent=None):
self.last_time = datetime.now()
self.preferences_action, self.quit_action = actions self.preferences_action, self.quit_action = actions
self.spare_servers = [] self.spare_servers = []
self.must_restart_before_config = False
MainWindow.__init__(self, opts, parent) MainWindow.__init__(self, opts, parent)
# Initialize fontconfig in a separate thread as this can be a lengthy # Initialize fontconfig in a separate thread as this can be a lengthy
# process if run for the first time on this machine # process if run for the first time on this machine
@ -143,6 +148,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.setupUi(self) self.setupUi(self)
self.setWindowTitle(__appname__) self.setWindowTitle(__appname__)
self.restriction_count_of_books_in_view = 0
self.restriction_count_of_books_in_library = 0
self.restriction_in_effect = False
self.search.initialize('main_search_history', colorize=True, self.search.initialize('main_search_history', colorize=True,
help_text=_('Search (For Advanced Search click the button to the left)')) help_text=_('Search (For Advanced Search click the button to the left)'))
self.connect(self.clear_button, SIGNAL('clicked()'), self.search_clear) self.connect(self.clear_button, SIGNAL('clicked()'), self.search_clear)
@ -501,6 +509,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.search_done)), self.search_done)),
('connect_to_book_display', ('connect_to_book_display',
(self.status_bar.book_info.show_data,)), (self.status_bar.book_info.show_data,)),
('connect_to_restriction_set',
(self.tags_view,)),
]: ]:
for view in (self.library_view, self.memory_view, self.card_a_view, self.card_b_view): for view in (self.library_view, self.memory_view, self.card_a_view, self.card_b_view):
getattr(view, func)(*args) getattr(view, func)(*args)
@ -540,8 +550,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
db = LibraryDatabase2(self.library_path) db = LibraryDatabase2(self.library_path)
self.library_view.set_database(db) self.library_view.set_database(db)
prefs['library_path'] = self.library_path prefs['library_path'] = self.library_path
self.library_view.sortByColumn(*dynamic.get('sort_column', self.library_view.restore_sort_at_startup(dynamic.get('sort_history', [('timestamp', Qt.DescendingOrder)]))
('timestamp', Qt.DescendingOrder)))
if not self.library_view.restore_column_widths(): if not self.library_view.restore_column_widths():
self.library_view.resizeColumnsToContents() self.library_view.resizeColumnsToContents()
self.search.setFocus(Qt.OtherFocusReason) self.search.setFocus(Qt.OtherFocusReason)
@ -551,10 +560,20 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.tags_view.setVisible(False) self.tags_view.setVisible(False)
self.tag_match.setVisible(False) self.tag_match.setVisible(False)
self.popularity.setVisible(False) self.popularity.setVisible(False)
self.tags_view.set_database(db, self.tag_match, self.popularity) self.restriction_label.setVisible(False)
self.edit_categories.setVisible(False)
self.search_restriction.setVisible(False)
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_edit_categories)
self.tags_view.set_database(db, self.tag_match, self.popularity, self.search_restriction)
self.connect(self.tags_view, self.connect(self.tags_view,
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'), SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
self.search.search_from_tags) self.search.search_from_tags)
self.connect(self.tags_view,
SIGNAL('restriction_set(PyQt_PyObject)'),
self.saved_search.clear_to_help)
self.connect(self.tags_view,
SIGNAL('restriction_set(PyQt_PyObject)'),
self.mark_restriction_set)
self.connect(self.tags_view, self.connect(self.tags_view,
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'), SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
self.saved_search.clear_to_help) self.saved_search.clear_to_help)
@ -567,8 +586,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
SIGNAL('count_changed(int)'), self.location_view.count_changed) SIGNAL('count_changed(int)'), self.location_view.count_changed)
self.connect(self.library_view.model(), SIGNAL('count_changed(int)'), self.connect(self.library_view.model(), SIGNAL('count_changed(int)'),
self.tags_view.recount, Qt.QueuedConnection) self.tags_view.recount, Qt.QueuedConnection)
self.connect(self.search, SIGNAL('cleared()'), self.tags_view_clear) self.connect(self.library_view.model(), SIGNAL('count_changed(int)'),
self.connect(self.saved_search, SIGNAL('changed()'), self.tags_view.recount, Qt.QueuedConnection) self.restriction_count_changed, Qt.QueuedConnection)
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
self.connect(self.saved_search, SIGNAL('changed()'), self.tags_view.saved_searches_changed, Qt.QueuedConnection)
if not gprefs.get('quick_start_guide_added', False): if not gprefs.get('quick_start_guide_added', False):
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember']) mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
@ -618,7 +639,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.resize(self.width(), self._calculated_available_height) self.resize(self.width(), self._calculated_available_height)
self.search.setMaximumWidth(self.width()-150) self.search.setMaximumWidth(self.width()-150)
if config['autolaunch_server']: if config['autolaunch_server']:
from calibre.library.server import start_threaded_server from calibre.library.server import start_threaded_server
from calibre.library import server_config from calibre.library import server_config
@ -658,6 +678,12 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
height = v.rowHeight(0) height = v.rowHeight(0)
self.library_view.verticalHeader().setDefaultSectionSize(height) self.library_view.verticalHeader().setDefaultSectionSize(height)
def do_edit_categories(self):
d = TagCategories(self, self.library_view.model().db)
d.exec_()
if d.result() == d.Accepted:
self.tags_view.set_new_model()
self.tags_view.recount()
def resizeEvent(self, ev): def resizeEvent(self, ev):
MainWindow.resizeEvent(self, ev) MainWindow.resizeEvent(self, ev)
@ -809,23 +835,68 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.tags_view.setVisible(True) self.tags_view.setVisible(True)
self.tag_match.setVisible(True) self.tag_match.setVisible(True)
self.popularity.setVisible(True) self.popularity.setVisible(True)
self.restriction_label.setVisible(True)
self.edit_categories.setVisible(True)
self.search_restriction.setVisible(True)
self.tags_view.setFocus(Qt.OtherFocusReason) self.tags_view.setFocus(Qt.OtherFocusReason)
else: else:
self.tags_view.setVisible(False) self.tags_view.setVisible(False)
self.tag_match.setVisible(False) self.tag_match.setVisible(False)
self.popularity.setVisible(False) self.popularity.setVisible(False)
self.restriction_label.setVisible(False)
self.edit_categories.setVisible(False)
self.search_restriction.setVisible(False)
def tags_view_clear(self): '''
self.search_count.setText(_("(all books)")) Handling of the count of books in a restricted view requires that
we capture the count after the initial restriction search. To so this,
we require that the restriction_set signal be issued before the search signal,
so that when the search_done happens and the count is displayed,
we can grab the count. This works because the search box is cleared
when a restriction is set, so that first search will find all books.
Adding and deleting books creates another complexity. When added, they are
displayed regardless of whether they match the restriction. However, if they
do not, they are removed at the next search. The counts must take this
behavior into effect.
'''
def restriction_count_changed(self, c):
self.restriction_count_of_books_in_view += c - self.restriction_count_of_books_in_library
self.restriction_count_of_books_in_library = c
if self.restriction_in_effect:
self.set_number_of_books_shown(all='not used', compute_count=False)
def mark_restriction_set(self, r):
self.restriction_in_effect = False if r is None or not r else True
def set_number_of_books_shown(self, all, compute_count):
if self.restriction_in_effect:
if compute_count:
self.restriction_count_of_books_in_view = self.current_view().row_count()
t = _("({0} of {1})").format(self.current_view().row_count(),
self.restriction_count_of_books_in_view)
self.search_count.setStyleSheet('QLabel { background-color: yellow; }')
else: # No restriction
if all == 'yes':
t = _("(all books)")
else:
t = _("({0} of all)").format(self.current_view().row_count())
self.search_count.setStyleSheet('QLabel { background-color: white; }')
self.search_count.setText(t)
def search_box_cleared(self):
self.set_number_of_books_shown(all='yes', compute_count=True)
self.tags_view.clear() self.tags_view.clear()
self.saved_search.clear_to_help()
def search_clear(self): def search_clear(self):
self.search_count.setText(_("(all books)")) self.set_number_of_books_shown(all='yes', compute_count=True)
self.search.clear() self.search.clear()
def search_done(self, view, ok): def search_done(self, view, ok):
if view is self.current_view(): if view is self.current_view():
self.search_count.setText(_("(%d found)") % self.current_view().row_count()) self.set_number_of_books_shown(all='no', compute_count=False)
self.search.search_done(ok) self.search.search_done(ok)
def sync_cf_to_listview(self, current, previous): def sync_cf_to_listview(self, current, previous):
@ -2174,7 +2245,12 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
_('Cannot configure while there are running jobs.')) _('Cannot configure while there are running jobs.'))
d.exec_() d.exec_()
return return
d = ConfigDialog(self, self.library_view.model().db, if self.must_restart_before_config:
d = error_dialog(self, _('Cannot configure'),
_('Cannot configure before calibre is restarted.'))
d.exec_()
return
d = ConfigDialog(self, self.library_view.model(),
server=self.content_server) server=self.content_server)
d.exec_() d.exec_()
self.content_server = d.server self.content_server = d.server
@ -2189,15 +2265,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
_('Save only %s format to disk')% _('Save only %s format to disk')%
prefs['output_format'].upper()) prefs['output_format'].upper())
self.library_view.model().read_config() self.library_view.model().read_config()
self.library_view.model().refresh()
self.library_view.model().research()
self.tags_view.set_new_model() # in case columns changed
self.tags_view.recount()
self.create_device_menu() self.create_device_menu()
if not patheq(self.library_path, d.database_location): if not patheq(self.library_path, d.database_location):
newloc = d.database_location newloc = d.database_location
move_library(self.library_path, newloc, self, move_library(self.library_path, newloc, self,
self.library_moved) self.library_moved)
def library_moved(self, newloc): def library_moved(self, newloc):
if newloc is None: return if newloc is None: return
db = LibraryDatabase2(newloc) db = LibraryDatabase2(newloc)
@ -2374,7 +2452,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
def write_settings(self): def write_settings(self):
config.set('main_window_geometry', self.saveGeometry()) config.set('main_window_geometry', self.saveGeometry())
dynamic.set('sort_column', self.library_view.model().sorted_on) dynamic.set('sort_history', self.library_view.model().sort_history)
dynamic.set('tag_view_visible', self.tags_view.isVisible()) dynamic.set('tag_view_visible', self.tags_view.isVisible())
dynamic.set('cover_flow_visible', self.cover_flow.isVisible()) dynamic.set('cover_flow_visible', self.cover_flow.isVisible())
self.library_view.write_settings() self.library_view.write_settings()

View File

@ -8,12 +8,15 @@ __docformat__ = 'restructuredtext en'
import collections, glob, os, re, itertools, functools import collections, glob, os, re, itertools, functools
from itertools import repeat from itertools import repeat
from datetime import timedelta
from PyQt4.QtCore import QThread, QReadWriteLock from PyQt4.QtCore import QThread, QReadWriteLock
from PyQt4.QtGui import QImage from PyQt4.QtGui import QImage
from calibre.utils.config import tweaks, prefs
from calibre.utils.date import parse_date, now
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
from calibre.utils.date import parse_date from calibre.utils.pyparsing import ParseException
class CoverCache(QThread): class CoverCache(QThread):
@ -146,6 +149,14 @@ class ResultCache(SearchQueryParser):
''' '''
Stores sorted and filtered metadata in memory. Stores sorted and filtered metadata in memory.
''' '''
def __init__(self, FIELD_MAP, cc_label_map):
self.FIELD_MAP = FIELD_MAP
self.custom_column_label_map = cc_label_map
self._map = self._map_filtered = self._data = []
self.first_sort = True
self.search_restriction = ''
SearchQueryParser.__init__(self, [c for c in cc_label_map])
self.build_relop_dict()
def build_relop_dict(self): def build_relop_dict(self):
''' '''
@ -194,13 +205,6 @@ class ResultCache(SearchQueryParser):
self.search_relops = {'=':[1, relop_eq], '>':[1, relop_gt], '<':[1, relop_lt], \ self.search_relops = {'=':[1, relop_eq], '>':[1, relop_gt], '<':[1, relop_lt], \
'!=':[2, relop_ne], '>=':[2, relop_ge], '<=':[2, relop_le]} '!=':[2, relop_ne], '>=':[2, relop_ge], '<=':[2, relop_le]}
def __init__(self, FIELD_MAP):
self.FIELD_MAP = FIELD_MAP
self._map = self._map_filtered = self._data = []
self.first_sort = True
SearchQueryParser.__init__(self)
self.build_relop_dict()
def __getitem__(self, row): def __getitem__(self, row):
return self._data[self._map_filtered[row]] return self._data[self._map_filtered[row]]
@ -214,13 +218,8 @@ class ResultCache(SearchQueryParser):
def universal_set(self): def universal_set(self):
return set([i[0] for i in self._data if i is not None]) return set([i[0] for i in self._data if i is not None])
def get_matches(self, location, query): def get_matches_dates(self, location, query):
matches = set([]) matches = set([])
if query and query.strip():
location = location.lower().strip()
### take care of dates special case
if location in ('pubdate', 'date'):
if len(query) < 2: if len(query) < 2:
return matches return matches
relop = None relop = None
@ -229,16 +228,54 @@ class ResultCache(SearchQueryParser):
(p, relop) = self.search_relops[k] (p, relop) = self.search_relops[k]
query = query[p:] query = query[p:]
if relop is None: if relop is None:
return matches (p, relop) = self.search_relops['=']
if location in self.custom_column_label_map:
loc = self.FIELD_MAP[self.custom_column_label_map[location]['num']]
else:
loc = self.FIELD_MAP[{'date':'timestamp', 'pubdate':'pubdate'}[location]] loc = self.FIELD_MAP[{'date':'timestamp', 'pubdate':'pubdate'}[location]]
if query == _('today'):
qd = now()
field_count = 3
elif query == _('yesterday'):
qd = now() - timedelta(1)
field_count = 3
elif query == _('thismonth'):
qd = now()
field_count = 2
elif query.endswith(_('daysago')):
num = query[0:-len(_('daysago'))]
try:
qd = now() - timedelta(int(num))
except:
raise ParseException(query, len(query), 'Number conversion error', self)
field_count = 3
else:
try:
qd = parse_date(query) qd = parse_date(query)
except:
raise ParseException(query, len(query), 'Date conversion error', self)
if '-' in query:
field_count = query.count('-') + 1 field_count = query.count('-') + 1
else:
field_count = query.count('/') + 1
for item in self._data: for item in self._data:
if item is None: continue if item is None or item[loc] is None: continue
if relop(item[loc], qd, field_count): if relop(item[loc], qd, field_count):
matches.add(item[0]) matches.add(item[0])
return matches return matches
def get_matches(self, location, query):
matches = set([])
if query and query.strip():
location = location.lower().strip()
### take care of dates special case
if (location in ('pubdate', 'date')) or \
((location in self.custom_column_label_map) and \
self.custom_column_label_map[location]['datatype'] == 'datetime'):
return self.get_matches_dates(location, query)
### everything else ### everything else
matchkind = CONTAINS_MATCH matchkind = CONTAINS_MATCH
if (len(query) > 1): if (len(query) > 1):
@ -257,19 +294,39 @@ class ResultCache(SearchQueryParser):
query = query.decode('utf-8') query = query.decode('utf-8')
if location in ('tag', 'author', 'format', 'comment'): if location in ('tag', 'author', 'format', 'comment'):
location += 's' location += 's'
all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series', 'formats', 'isbn', 'rating', 'cover') all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series', 'formats', 'isbn', 'rating', 'cover')
MAP = {} MAP = {}
for x in all:
for x in all: # get the db columns for the standard searchables
MAP[x] = self.FIELD_MAP[x] MAP[x] = self.FIELD_MAP[x]
IS_CUSTOM = []
for x in range(len(self.FIELD_MAP)): # build a list containing '' the size of FIELD_MAP
IS_CUSTOM.append('')
IS_CUSTOM[self.FIELD_MAP['rating']] = 'rating' # normal and custom ratings columns use the same code
for x in self.custom_column_label_map: # add custom columns to MAP. Put the column's type into IS_CUSTOM
if self.custom_column_label_map[x]['datatype'] != "datetime":
MAP[x] = self.FIELD_MAP[self.custom_column_label_map[x]['num']]
IS_CUSTOM[MAP[x]] = self.custom_column_label_map[x]['datatype']
EXCLUDE_FIELDS = [MAP['rating'], MAP['cover']] EXCLUDE_FIELDS = [MAP['rating'], MAP['cover']]
SPLITABLE_FIELDS = [MAP['authors'], MAP['tags'], MAP['formats']] SPLITABLE_FIELDS = [MAP['authors'], MAP['tags'], MAP['formats']]
for x in self.custom_column_label_map:
if self.custom_column_label_map[x]['is_multiple']:
SPLITABLE_FIELDS.append(MAP[x])
location = [location] if location != 'all' else list(MAP.keys()) location = [location] if location != 'all' else list(MAP.keys())
for i, loc in enumerate(location): for i, loc in enumerate(location):
location[i] = MAP[loc] location[i] = MAP[loc]
try: try:
rating_query = int(query) * 2 rating_query = int(query) * 2
except: except:
rating_query = None rating_query = None
# get the tweak here so that the string lookup and compare aren't in the loop
bools_are_tristate = tweaks['bool_custom_columns_are_tristate'] == 'yes'
for loc in location: for loc in location:
if loc == MAP['authors']: if loc == MAP['authors']:
q = query.replace(',', '|'); ### DB stores authors with commas changed to bars, so change query q = query.replace(',', '|'); ### DB stores authors with commas changed to bars, so change query
@ -278,14 +335,34 @@ class ResultCache(SearchQueryParser):
for item in self._data: for item in self._data:
if item is None: continue if item is None: continue
if not item[loc]:
if query == 'false': if IS_CUSTOM[loc] == 'bool': # complexity caused by the two-/three-value tweak
if isinstance(item[loc], basestring): v = item[loc]
if item[loc].strip() != '': if not bools_are_tristate:
continue if v is None or not v: # item is None or set to false
if q in [_('no'), _('unchecked'), 'false']:
matches.add(item[0])
else: # item is explicitly set to true
if q in [_('yes'), _('checked'), 'true']:
matches.add(item[0])
else:
if v is None:
if q in [_('empty'), _('blank'), 'false']:
matches.add(item[0])
elif not v: # is not None and false
if q in [_('no'), _('unchecked'), 'true']:
matches.add(item[0])
else: # item is not None and true
if q in [_('yes'), _('checked'), 'true']:
matches.add(item[0]) matches.add(item[0])
continue continue
continue ### item is empty. No possible matches below
if not item[loc]:
if q == 'false':
matches.add(item[0])
continue # item is empty. No possible matches below
if q == 'false': # Field has something in it, so a false query does not match
continue
if q == 'true': if q == 'true':
if isinstance(item[loc], basestring): if isinstance(item[loc], basestring):
@ -293,12 +370,30 @@ class ResultCache(SearchQueryParser):
continue continue
matches.add(item[0]) matches.add(item[0])
continue continue
if rating_query and loc == MAP['rating'] and rating_query == int(item[loc]):
if IS_CUSTOM[loc] == 'rating':
if rating_query and rating_query == int(item[loc]):
matches.add(item[0]) matches.add(item[0])
continue continue
try: # a conversion below might fail
if IS_CUSTOM[loc] == 'float':
if float(query) == item[loc]: # relationals not supported
matches.add(item[0])
continue
if IS_CUSTOM[loc] == 'int':
if int(query) == item[loc]:
matches.add(item[0])
continue
except:
continue ## A conversion threw an exception. Because of the type, no further match possible
if loc not in EXCLUDE_FIELDS: if loc not in EXCLUDE_FIELDS:
if loc in SPLITABLE_FIELDS: if loc in SPLITABLE_FIELDS:
vals = item[loc].split(',') ### check individual tags/authors/formats, not the long string if IS_CUSTOM[loc]:
vals = item[loc].split('|')
else:
vals = item[loc].split(',')
else: else:
vals = [item[loc]] ### make into list to make _match happy vals = [item[loc]] ### make into list to make _match happy
if _match(q, vals, matchkind): if _match(q, vals, matchkind):
@ -342,8 +437,7 @@ class ResultCache(SearchQueryParser):
''' '''
for id in ids: for id in ids:
try: try:
self._data[id] = db.conn.get('SELECT * from meta2 WHERE id=?', self._data[id] = db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0]
(id,))[0]
self._data[id].append(db.has_cover(id, index_is_id=True)) self._data[id].append(db.has_cover(id, index_is_id=True))
except IndexError: except IndexError:
return None return None
@ -399,6 +493,12 @@ class ResultCache(SearchQueryParser):
asstr else cmp(self._data[x][loc], self._data[y][loc]) asstr else cmp(self._data[x][loc], self._data[y][loc])
except AttributeError: # Some entries may be None except AttributeError: # Some entries may be None
ans = cmp(self._data[x][loc], self._data[y][loc]) ans = cmp(self._data[x][loc], self._data[y][loc])
except TypeError: ## raised when a datetime is None
if self._data[x][loc] is None:
if self._data[y][loc] is None:
return 0 # Both None. Return eq
return 1 # x is None, y not. Return gt
return -1 # x is not None and (therefore) y is. return lt
if subsort and ans == 0: if subsort and ans == 0:
return cmp(self._data[x][11].lower(), self._data[y][11].lower()) return cmp(self._data[x][11].lower(), self._data[y][11].lower())
return ans return ans
@ -410,21 +510,35 @@ class ResultCache(SearchQueryParser):
if field == 'date': field = 'timestamp' if field == 'date': field = 'timestamp'
elif field == 'title': field = 'sort' elif field == 'title': field = 'sort'
elif field == 'authors': field = 'author_sort' elif field == 'authors': field = 'author_sort'
as_string = field not in ('size', 'rating', 'timestamp')
if field in self.custom_column_label_map:
as_string = self.custom_column_label_map[field]['datatype'] in ('comments', 'text')
field = self.custom_column_label_map[field]['num']
if self.first_sort: if self.first_sort:
subsort = True subsort = True
self.first_sort = False self.first_sort = False
fcmp = self.seriescmp if field == 'series' else \ fcmp = self.seriescmp if field == 'series' else \
functools.partial(self.cmp, self.FIELD_MAP[field], subsort=subsort, functools.partial(self.cmp, self.FIELD_MAP[field], subsort=subsort,
asstr=field not in ('size', 'rating', 'timestamp')) asstr=as_string)
self._map.sort(cmp=fcmp, reverse=not ascending) self._map.sort(cmp=fcmp, reverse=not ascending)
self._map_filtered = [id for id in self._map if id in self._map_filtered] self._map_filtered = [id for id in self._map if id in self._map_filtered]
def search(self, query): def search(self, query, return_matches = False):
if not query or not query.strip(): if not query or not query.strip():
q = self.search_restriction
else:
q = '%s (%s)' % (self.search_restriction, query)
if not q:
if return_matches:
return list(self.map) # when return_matches, do not update the maps!
self._map_filtered = list(self._map) self._map_filtered = list(self._map)
return return []
matches = sorted(self.parse(query)) matches = sorted(self.parse(q))
if return_matches:
return [id for id in self._map if id in matches]
self._map_filtered = [id for id in self._map if id in matches] self._map_filtered = [id for id in self._map if id in matches]
return []
def set_search_restriction(self, s):
self.search_restriction = '' if not s else 'search:"%s"' % (s.strip())

View File

@ -190,7 +190,7 @@ class CustomColumns(object):
(label, num)) (label, num))
changed = True changed = True
if is_editable is not None: if is_editable is not None:
self.conn.execute('UPDATE custom_columns SET is_editable=? WHERE id=?', self.conn.execute('UPDATE custom_columns SET editable=? WHERE id=?',
(bool(is_editable), num)) (bool(is_editable), num))
self.custom_column_num_map[num]['is_editable'] = bool(is_editable) self.custom_column_num_map[num]['is_editable'] = bool(is_editable)
changed = True changed = True

View File

@ -56,16 +56,15 @@ def delete_tree(path, permanent=False):
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
class Tag(object): class Tag(object):
def __init__(self, name, id=None, count=0, state=0, tooltip=None): def __init__(self, name, id=None, count=0, state=0, tooltip=None, icon=None):
self.name = name self.name = name
self.id = id self.id = id
self.count = count self.count = count
self.state = state self.state = state
self.tooltip = tooltip self.tooltip = tooltip
self.icon = icon
def __unicode__(self): def __unicode__(self):
return u'%s:%s:%s:%s:%s'%(self.name, self.count, self.id, self.state, self.tooltip) return u'%s:%s:%s:%s:%s'%(self.name, self.count, self.id, self.state, self.tooltip)
@ -186,7 +185,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.executescript(script) self.conn.executescript(script)
self.conn.commit() self.conn.commit()
self.data = ResultCache(self.FIELD_MAP) self.data = ResultCache(self.FIELD_MAP, self.custom_column_label_map)
self.search = self.data.search self.search = self.data.search
self.refresh = functools.partial(self.data.refresh, self) self.refresh = functools.partial(self.data.refresh, self)
self.sort = self.data.sort self.sort = self.data.sort
@ -576,12 +575,41 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def get_recipe(self, id): def get_recipe(self, id):
return self.conn.get('SELECT script FROM feeds WHERE id=?', (id,), all=False) return self.conn.get('SELECT script FROM feeds WHERE id=?', (id,), all=False)
def get_categories(self, sort_on_count=False): def get_categories(self, sort_on_count=False, ids=None, icon_map=None):
orig_category_columns = {'tags': ['tag', 'name'],
'series': ['series', 'name'],
'publishers': ['publisher', 'name'],
'authors': ['author', 'name']} # 'news' is added below
cat_cols = {}
def create_filtered_views(self, ids):
def create_tag_browser_view(table_name, column_name, view_column_name):
script = ('''
CREATE TEMP VIEW IF NOT EXISTS tag_browser_filtered_{tn} AS SELECT
id,
{vcn},
(SELECT COUNT(books_{tn}_link.id) FROM books_{tn}_link WHERE {cn}={tn}.id and books_list_filter(book)) count
FROM {tn};
'''.format(tn=table_name, cn=column_name, vcn=view_column_name))
self.conn.executescript(script)
self.cat_cols = {}
for tn,cn in orig_category_columns.iteritems():
create_tag_browser_view(tn, cn[0], cn[1])
cat_cols[tn] = cn
for i,v in self.custom_column_num_map.iteritems():
if v['datatype'] == 'text':
tn = 'custom_column_{0}'.format(i)
create_tag_browser_view(tn, 'value', 'value')
cat_cols[tn] = [v['label'], 'value']
cat_cols['news'] = ['news', 'name']
self.conn.executescript(u''' self.conn.executescript(u'''
CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT
id, id,
name, name,
(SELECT COUNT(id) FROM books_tags_link WHERE tag=x.id) count (SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id) count
FROM tags as x WHERE name!="{0}" AND id IN FROM tags as x WHERE name!="{0}" AND id IN
(SELECT DISTINCT tag FROM books_tags_link WHERE book IN (SELECT DISTINCT tag FROM books_tags_link WHERE book IN
(SELECT DISTINCT book FROM books_tags_link WHERE tag IN (SELECT DISTINCT book FROM books_tags_link WHERE tag IN
@ -589,21 +617,56 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
'''.format(_('News'))) '''.format(_('News')))
self.conn.commit() self.conn.commit()
self.conn.executescript(u'''
CREATE TEMP VIEW IF NOT EXISTS tag_browser_filtered_news AS SELECT DISTINCT
id,
name,
(SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id and books_list_filter(book)) count
FROM tags as x WHERE name!="{0}" AND id IN
(SELECT DISTINCT tag FROM books_tags_link WHERE book IN
(SELECT DISTINCT book FROM books_tags_link WHERE tag IN
(SELECT id FROM tags WHERE name="{0}")));
'''.format(_('News')))
self.conn.commit()
if ids is not None:
s_ids = set(ids)
else:
s_ids = None
self.conn.create_function('books_list_filter', 1, lambda(id): 1 if id in s_ids else 0)
create_filtered_views(self, ids)
categories = {} categories = {}
for x in ('tags', 'series', 'news', 'publishers', 'authors'): for tn,cn in cat_cols.iteritems():
query = 'SELECT id,name,count FROM tag_browser_'+x if ids is None:
query = 'SELECT id, {0}, count FROM tag_browser_{1}'.format(cn[1], tn)
else:
query = 'SELECT id, {0}, count FROM tag_browser_filtered_{1}'.format(cn[1], tn)
if sort_on_count: if sort_on_count:
query += ' ORDER BY count DESC' query += ' ORDER BY count DESC'
else: else:
query += ' ORDER BY name ASC' query += ' ORDER BY {0} ASC'.format(cn[1])
data = self.conn.get(query) data = self.conn.get(query)
category = x if x in ('series', 'news') else x[:-1] category = cn[0]
categories[category] = [Tag(r[1], count=r[2], id=r[0]) for r in data] icon = icon_map[category] if category in icon_map else icon_map['*custom']
if ids is None: # no filtering
categories[category] = [Tag(r[1], count=r[2], id=r[0], icon=icon)
for r in data]
else: # filter out zero-count tags
categories[category] = [Tag(r[1], count=r[2], id=r[0], icon=icon)
for r in data if r[2] > 0]
categories['format'] = [] categories['format'] = []
for fmt in self.conn.get('SELECT DISTINCT format FROM data'): for fmt in self.conn.get('SELECT DISTINCT format FROM data'):
fmt = fmt[0] fmt = fmt[0]
count = self.conn.get('SELECT COUNT(id) FROM data WHERE format="%s"'%fmt, if ids is not None:
count = self.conn.get('''SELECT COUNT(id)
FROM data
WHERE format="%s" and books_list_filter(id)'''%fmt,
all=False)
else:
count = self.conn.get('''SELECT COUNT(id)
FROM data
WHERE format="%s"'''%fmt,
all=False) all=False)
categories['format'].append(Tag(fmt, count=count)) categories['format'].append(Tag(fmt, count=count))
@ -612,7 +675,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
reverse=True) reverse=True)
else: else:
categories['format'].sort(cmp=lambda x,y:cmp(x.name, y.name)) categories['format'].sort(cmp=lambda x,y:cmp(x.name, y.name))
return categories return categories
def tags_older_than(self, tag, delta): def tags_older_than(self, tag, delta):

View File

@ -24,6 +24,13 @@ class SafeLocalTimeZone(tzlocal):
pass pass
return False return False
def compute_locale_info_for_parse_date():
dt = datetime.strptime('1/5/2000', "%x")
if dt.month == 5:
return True
return False
parse_date_day_first = compute_locale_info_for_parse_date()
utc_tz = _utc_tz = tzutc() utc_tz = _utc_tz = tzutc()
local_tz = _local_tz = SafeLocalTimeZone() local_tz = _local_tz = SafeLocalTimeZone()
@ -44,7 +51,7 @@ def parse_date(date_string, assume_utc=False, as_utc=True, default=None):
func = datetime.utcnow if assume_utc else datetime.now func = datetime.utcnow if assume_utc else datetime.now
default = func().replace(hour=0, minute=0, second=0, microsecond=0, default = func().replace(hour=0, minute=0, second=0, microsecond=0,
tzinfo=_utc_tz if assume_utc else _local_tz) tzinfo=_utc_tz if assume_utc else _local_tz)
dt = parse(date_string, default=default) dt = parse(date_string, default=default, dayfirst=parse_date_day_first)
if dt.tzinfo is None: if dt.tzinfo is None:
dt = dt.replace(tzinfo=_utc_tz if assume_utc else _local_tz) dt = dt.replace(tzinfo=_utc_tz if assume_utc else _local_tz)
return dt.astimezone(_utc_tz if as_utc else _local_tz) return dt.astimezone(_utc_tz if as_utc else _local_tz)

View File

@ -116,13 +116,12 @@ class SearchQueryParser(object):
failed.append(test[0]) failed.append(test[0])
return failed return failed
def __init__(self, test=False): def __init__(self, custcols=[], test=False):
self._tests_failed = False self._tests_failed = False
# Define a token # Define a token
locations = map(lambda x : CaselessLiteral(x)+Suppress(':'), standard_locations = map(lambda x : CaselessLiteral(x)+Suppress(':'), self.LOCATIONS+custcols)
self.LOCATIONS)
location = NoMatch() location = NoMatch()
for l in locations: for l in standard_locations:
location |= l location |= l
location = Optional(location, default='all') location = Optional(location, default='all')
word_query = CharsNotIn(string.whitespace + '()') word_query = CharsNotIn(string.whitespace + '()')
@ -176,14 +175,20 @@ class SearchQueryParser(object):
def parse(self, query): def parse(self, query):
# empty the list of searches used for recursion testing # empty the list of searches used for recursion testing
self.recurse_level = 0
self.searches_seen = set([]) self.searches_seen = set([])
return self._parse(query) return self._parse(query)
# this parse is used internally because it doesn't clear the # this parse is used internally because it doesn't clear the
# recursive search test list # recursive search test list. However, we permit seeing the
# same search a few times because the search might appear within
# another search.
def _parse(self, query): def _parse(self, query):
self.recurse_level += 1
res = self._parser.parseString(query)[0] res = self._parser.parseString(query)[0]
return self.evaluate(res) t = self.evaluate(res)
self.recurse_level -= 1
return t
def method(self, group_name): def method(self, group_name):
return getattr(self, 'evaluate_'+group_name) return getattr(self, 'evaluate_'+group_name)
@ -207,12 +212,12 @@ class SearchQueryParser(object):
location = argument[0] location = argument[0]
query = argument[1] query = argument[1]
if location.lower() == 'search': if location.lower() == 'search':
# print "looking for named search " + query
if query.startswith('='): if query.startswith('='):
query = query[1:] query = query[1:]
try: try:
if query in self.searches_seen: if query in self.searches_seen:
raise ParseException(query, len(query), 'undefined saved search', self) raise ParseException(query, len(query), 'undefined saved search', self)
if self.recurse_level > 5:
self.searches_seen.add(query) self.searches_seen.add(query)
return self._parse(saved_searches.lookup(query)) return self._parse(saved_searches.lookup(query))
except: # convert all exceptions (e.g., missing key) to a parse error except: # convert all exceptions (e.g., missing key) to a parse error