Custom column changes, etc. Also removed a print statement

This commit is contained in:
Charles Haley 2010-04-18 17:08:27 +01:00
parent 597352a747
commit e61e40bdf0
21 changed files with 2129 additions and 265 deletions

View File

@ -25,3 +25,20 @@ series_index_auto_increment = 'next'
# copy : copy author to author_sort without modification
# comma : use 'copy' if there is a ',' in the name, otherwise use '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

View File

@ -13,6 +13,7 @@ from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500'
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.localization import set_qt_translator
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
@ -95,6 +96,7 @@ def _config():
help=_('Overwrite author and title with new metadata'))
c.add_opt('enforce_cpu_limit', default=True,
help=_('Limit max simultaneous jobs to number of CPUs'))
c.add_opt('tag_categories', default={}, help=_('User-created tag categories'))
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'
__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, \
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
@ -8,10 +8,11 @@ from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
SIGNAL, QThread, Qt, QSize, QVariant, QUrl, \
QModelIndex, QAbstractTableModel, \
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.create_custom_column import CreateCustomColumn
from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, \
ALL_COLUMNS, NONE, info_dialog, choose_files, \
warning_dialog, ResizableDialog
@ -90,7 +91,6 @@ class ConfigTabs(QTabWidget):
widget.commit(save_defaults=True)
return True
class PluginModel(QAbstractItemModel):
def __init__(self, *args):
@ -328,14 +328,16 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
def category_current_changed(self, n, p):
self.stackedWidget.setCurrentIndex(n.row())
def __init__(self, window, db, server=None):
ResizableDialog.__init__(self, window)
def __init__(self, parent, model, server=None):
ResizableDialog.__init__(self, parent)
self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
self._category_model = CategoryModel()
self.category_view.currentChanged = self.category_current_changed
self.category_view.setModel(self._category_model)
self.db = db
self.parent = parent
self.model = model
self.db = model.db
self.server = server
path = prefs['library_path']
self.location.setText(path if path else '')
@ -359,15 +361,27 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.roman_numerals.setChecked(rn)
self.new_version_notification.setChecked(config['new_version_notification'])
column_map = config['column_map']
for col in column_map + [i for i in ALL_COLUMNS if i not in column_map]:
item = QListWidgetItem(BooksModel.headers[col], self.columns)
# Set up columns
# Make copies of maps so that internal changes aren't put into the real maps
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.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_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']
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:
self.language.addItem(item[1], QVariant(item[0]))
exts = set([])
for ext in BOOK_EXTENSIONS:
ext = ext.lower()
@ -633,6 +646,31 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.columns.insertItem(idx+1, self.columns.takeItem(idx))
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):
from calibre.library.server import log_access_file, log_error_file
d = QDialog(self)
@ -702,7 +740,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
if dir:
self.location.setText(dir)
def accept(self):
mcs = unicode(self.max_cover_size.text()).strip()
if not re.match(r'\d+x\d+', mcs):
@ -720,17 +757,38 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
wl += 1
config['worker_limit'] = wl
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
config['new_version_notification'] = bool(self.new_version_notification.isChecked())
prefs['network_timeout'] = int(self.timeout.value())
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())]
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:
cols = ['title']
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['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
config['separate_cover_flow'] = bool(self.separate_cover_flow.isChecked())
@ -771,8 +829,16 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
d.exec_()
else:
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)
# 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):
def __init__(self, parent, db):

View File

@ -498,6 +498,87 @@
</property>
</spacer>
</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>
<widget class="QToolButton" name="column_down">
<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 is already used'))
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,172 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtGui import QDialog, QDialogButtonBox, QLineEdit, QComboBox
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 TagCategories(QDialog, Ui_TagCategories):
category_names = [_('Authors'), _('Series'), _('Publishers'), _('Tags')]
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.tags = []
self.all_items = {}
self.all_items['tag'] = sorted(self.db.all_tags(), cmp=lambda x,y: cmp(x.lower(), y.lower()))
self.all_items['author'] = sorted([i[1].replace('|', ',') for i in self.db.all_authors()],
cmp=lambda x,y: cmp(x.lower(), y.lower()))
self.all_items['publisher'] = sorted([i[1] for i in self.db.all_publishers()],
cmp=lambda x,y: cmp(x.lower(), y.lower()))
self.all_items['series'] = sorted([i[1] for i in self.db.all_series()],
cmp=lambda x,y: cmp(x.lower(), y.lower()))
self.current_cat_name = None
self.current_cat_label= None
self.category_label_to_name = {}
self.category_name_to_label = {}
for i in range(len(self.category_labels)):
self.category_label_to_name[self.category_labels[i]] = self.category_names[i]
self.category_name_to_label[self.category_names[i]] = self.category_labels[i]
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.delete_category_button, SIGNAL('clicked()'), self.del_category)
if islinux:
self.available_tags.itemDoubleClicked.connect(self.apply_tags)
else:
self.connect(self.available_tags, SIGNAL('itemActivated(QListWidgetItem*)'), self.apply_tags)
self.connect(self.applied_tags, SIGNAL('itemActivated(QListWidgetItem*)'), self.unapply_tags)
self.categories = dict.copy(config['tag_categories'])
if self.categories is None:
self.categories = {}
self.populate_category_list()
self.category_kind_box.clear()
for i in range(len(self.category_names)):
self.category_kind_box.addItem(self.category_names[i])
self.select_category(0)
def apply_tags(self, item=None):
if self.current_cat_name[0] is None:
return
items = self.available_tags.selectedItems() if item is None else [item]
for item in items:
tag = qstring_to_unicode(item.text())
if tag not in self.tags:
self.tags.append(tag)
self.available_tags.takeItem(self.available_tags.row(item))
self.tags.sort()
self.applied_tags.clear()
for tag in self.tags:
self.applied_tags.addItem(tag)
def unapply_tags(self, item=None):
items = self.applied_tags.selectedItems() if item is None else [item]
for item in items:
tag = qstring_to_unicode(item.text())
self.tags.remove(tag)
self.available_tags.addItem(tag)
self.tags.sort()
self.applied_tags.clear()
for tag in self.tags:
self.applied_tags.addItem(tag)
self.available_tags.sortItems()
def add_category(self):
self.save_category()
cat_name = qstring_to_unicode(self.input_box.text()).strip()
if cat_name == '':
return
cat_kind = unicode(self.category_kind_box.currentText())
r_cat_kind = self.category_name_to_label[cat_kind]
if r_cat_kind not in self.categories:
self.categories[r_cat_kind] = {}
if cat_name not in self.categories[r_cat_kind]:
self.category_box.clear()
self.category_kind_label.setText(cat_kind)
self.current_cat_name = cat_name
self.current_cat_label = r_cat_kind
self.categories[r_cat_kind][cat_name] = []
if len(self.tags):
self.clear_boxes(item_label=self.current_cat_label)
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 not confirm('<p>'+_('The current tag category will be '
'<b>permanently deleted</b>. Are you sure?')
+'</p>', 'tag_category_delete', self):
return
print 'here', self.current_category
if self.current_cat_name is not None:
if self.current_cat_name == unicode(self.category_box.currentText()):
del self.categories[self.current_cat_label][self.current_cat_name]
self.current_category = [None, None] ## order here is important. RemoveItem will put it back
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)
self.current_cat_label = str(self.category_box.itemData(idx).toString())
else:
self.current_cat_name = None
self.current_cat_label = None
self.clear_boxes(item_label=False)
if self.current_cat_label:
self.category_kind_label.setText(self.category_label_to_name[self.current_cat_label])
self.tags = self.categories[self.current_cat_label].get(self.current_cat_name, [])
# Must do two loops because obsolete values can be saved
# We need to show these to the user so they can be deleted if desired
for t in self.tags:
self.applied_tags.addItem(t)
for t in self.all_items[self.current_cat_label]:
if t not in self.tags:
self.available_tags.addItem(t)
else:
self.category_kind_label.setText('')
def clear_boxes(self, item_label = None):
self.tags = []
self.applied_tags.clear()
self.available_tags.clear()
if item_label:
for item in self.all_items[item_label]:
self.available_tags.addItem(item)
def accept(self):
self.save_category()
config['tag_categories'] = self.categories
QDialog.accept(self)
def save_category(self):
if self.current_cat_name is not None:
self.categories[self.current_cat_label][self.current_cat_name] = self.tags
def populate_category_list(self):
cat_list = {}
for c in self.categories:
for n in self.categories[c]:
if n.strip():
cat_list[n] = c
for n in sorted(cat_list.keys(), cmp=lambda x,y: cmp(x.lower(), y.lower())):
self.category_box.addItem(n, cat_list[n])

View File

@ -0,0 +1,427 @@
<?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 values</string>
</property>
<property name="buddy">
<cstring>available_tags</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_tags">
<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 values</string>
</property>
<property name="buddy">
<cstring>applied_tags</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_tags">
<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="4">
<widget class="QComboBox" name="category_kind_box">
<property name="toolTip">
<string>Select the content kind of the new category</string>
</property>
<item>
<property name="text">
<string>Author</string>
</property>
</item>
<item>
<property name="text">
<string>Series</string>
</property>
</item>
<item>
<property name="text">
<string>Formats</string>
</property>
</item>
<item>
<property name="text">
<string>Publishers</string>
</property>
</item>
<item>
<property name="text">
<string>Tags</string>
</property>
</item>
</widget>
</item>
<item row="1" column="3">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Category kind:</string>
</property>
<property name="buddy">
<cstring>category_kind_box</cstring>
</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="1">
<widget class="QLabel" name="category_kind_label">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Category kind: </string>
</property>
<property name="alignment">
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter</set>
</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,34 +1,36 @@
__license__ = 'GPL v3'
__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 math import cos, sin, pi
from contextlib import closing
from datetime import date
from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \
QItemDelegate, QPainterPath, QLinearGradient, QBrush, \
QPen, QStyle, QPainter, \
QPen, QStyle, QPainter, QIcon,\
QImage, QApplication, QMenu, \
QStyledItemDelegate, QCompleter
QStyledItemDelegate, QCompleter, QIntValidator, \
QPlainTextEdit, QDoubleValidator, QCheckBox, QMessageBox
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.ptempfile import PersistentTemporaryFile
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 import string_to_authors, fmt_sidx, authors_to_string
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
from calibre.ebooks.metadata import string_to_authors, fmt_sidx, \
authors_to_string
from calibre.utils.config import tweaks
from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, error_dialog
from calibre.gui2.dialogs.comments_dialog import CommentsDialog
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.pyparsing import ParseException
from calibre.utils.search_query_parser import SearchQueryParser
class LibraryDelegate(QItemDelegate):
class RatingDelegate(QItemDelegate):
COLOR = QColor("blue")
SIZE = 16
PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
@ -54,7 +56,10 @@ class LibraryDelegate(QItemDelegate):
return QSize(5*(self.SIZE), self.SIZE+4)
def paint(self, painter, option, index):
num = index.model().data(index, Qt.DisplayRole).toInt()[0]
if index.model().data(index, Qt.DisplayRole) is None:
num = 0
else:
num = index.model().data(index, Qt.DisplayRole).toInt()[0]
def draw_star():
painter.save()
painter.scale(self.factor, self.factor)
@ -95,7 +100,6 @@ class LibraryDelegate(QItemDelegate):
return sb
class DateDelegate(QStyledItemDelegate):
def displayText(self, val, locale):
d = val.toDate()
return d.toString('dd MMM yyyy')
@ -111,7 +115,6 @@ class DateDelegate(QStyledItemDelegate):
return qde
class PubDateDelegate(QStyledItemDelegate):
def displayText(self, val, locale):
return val.toDate().toString('MMM yyyy')
@ -123,7 +126,6 @@ class PubDateDelegate(QStyledItemDelegate):
return qde
class TextDelegate(QStyledItemDelegate):
def __init__(self, parent):
'''
Delegate for text data. If auto_complete_function needs to return a list
@ -147,7 +149,6 @@ class TextDelegate(QStyledItemDelegate):
return editor
class TagsDelegate(QStyledItemDelegate):
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
self.db = None
@ -157,17 +158,101 @@ class TagsDelegate(QStyledItemDelegate):
def createEditor(self, parent, option, index):
if self.db:
editor = TagsLineEdit(parent, self.db.all_tags())
col = index.model().column_map[index.column()]
if not index.model().is_custom_column(col):
editor = TagsLineEdit(parent, self.db.all_tags())
else:
editor = TagsLineEdit(parent, sorted(list(self.db.all_custom(label=col))))
return editor;
else:
editor = EnLineEdit(parent)
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):
about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted')
sorting_done = pyqtSignal(object, name='sortingDone')
headers = {
orig_headers = {
'title' : _("Title"),
'authors' : _("Author(s)"),
'size' : _("Size (MB)"),
@ -182,15 +267,22 @@ class BooksModel(QAbstractTableModel):
def __init__(self, parent=None, buffer=40):
QAbstractTableModel.__init__(self, parent)
self.db = None
self.column_map = config['column_map']
self.editable_cols = ['title', 'authors', 'rating', 'publisher',
'tags', 'series', 'timestamp', 'pubdate']
self.default_image = QImage(I('book.svg'))
self.sorted_on = ('timestamp', Qt.AscendingOrder)
self.sort_history = [self.sorted_on]
self.last_search = '' # The last search performed on this model
self.read_config()
self.column_map = []
self.headers = {}
self.buffer_size = buffer
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):
if self.cover_cache:
@ -198,15 +290,24 @@ class BooksModel(QAbstractTableModel):
def read_config(self):
self.use_roman_numbers = config['use_roman_numerals_for_series_number']
cols = config['column_map']
if cols != self.column_map:
self.column_map = cols
self.reset()
self.emit(SIGNAL('columns_sorted()'))
self.column_map = config['column_map'][:] # force a copy
self.headers = {}
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.reset()
self.emit(SIGNAL('columns_sorted()'))
def set_database(self, db):
self.db = db
self.custom_columns = self.db.custom_column_label_map
self.build_data_convertors()
self.read_config()
def refresh_ids(self, ids, current_row=-1):
rows = self.db.refresh_ids(ids)
@ -313,6 +414,8 @@ class BooksModel(QAbstractTableModel):
self.clear_caches()
self.reset()
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)
def refresh(self, reset=True):
@ -320,10 +423,8 @@ class BooksModel(QAbstractTableModel):
col = self.column_map.index(self.sorted_on[0])
except:
col = 0
self.db.refresh(field=self.column_map[col],
ascending=self.sorted_on[1]==Qt.AscendingOrder)
if reset:
self.reset()
self.db.refresh(field=None)
self.sort(col, self.sorted_on[1], reset=reset)
def resort(self, reset=True):
try:
@ -427,6 +528,7 @@ class BooksModel(QAbstractTableModel):
return ans
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 = [], []
if not rows_are_ids:
rows = [self.db.id(row.row()) for row in rows]
@ -559,75 +661,108 @@ class BooksModel(QAbstractTableModel):
return img
def build_data_convertors(self):
tidx = self.db.FIELD_MAP['title']
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]
def authors(r, idx=-1):
au = self.db.data[r][idx]
if au:
au = [a.strip().replace('|', ',') for a in au.split(',')]
return ' & '.join(au)
return QVariant(' & '.join(au))
else:
return None
def timestamp(r):
dt = self.db.data[r][tmdx]
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]
def tags(r, idx=-1):
tags = self.db.data[r][idx]
if tags:
return ', '.join(sorted(tags.split(',')))
return QVariant(', '.join(sorted(tags.split(','))))
return None
def series(r):
series = self.db.data[r][srdx]
def series(r, idx=-1, siix=-1):
series = self.db.data[r][idx]
if series:
idx = fmt_sidx(self.db.data[r][siix])
return series + ' [%s]'%idx
def size(r):
size = self.db.data[r][sidx]
return QVariant(series + ' [%s]'%idx)
return None
def size(r, idx=-1):
size = self.db.data[r][idx]
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):
val = self.db.data[r][idx]
if tweaks['bool_custom_columns_are_tristate'] == 'no':
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 = {
'title' : lambda r : self.db.data[r][tidx],
'authors' : authors,
'size' : size,
'timestamp': timestamp,
'pubdate' : pubdate,
'rating' : rating,
'publisher': publisher,
'tags' : tags,
'series' : series,
'title' : functools.partial(text_type, idx=self.db.FIELD_MAP['title'], mult=False),
'authors' : functools.partial(authors, idx=self.db.FIELD_MAP['authors']),
'size' : functools.partial(size, idx=self.db.FIELD_MAP['size']),
'timestamp': functools.partial(datetime_type, idx=self.db.FIELD_MAP['timestamp']),
'pubdate' : functools.partial(datetime_type, idx=self.db.FIELD_MAP['pubdate']),
'rating' : functools.partial(rating_type, idx=self.db.FIELD_MAP['rating']),
'publisher': functools.partial(text_type, idx=self.db.FIELD_MAP['title'], mult=False),
'tags' : functools.partial(tags, idx=self.db.FIELD_MAP['tags']),
'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)
elif datatype == 'rating':
self.dc[col] = functools.partial(rating_type, idx=idx)
else:
print 'What type is this?', col, datatype
def data(self, index, role):
if role in (Qt.DisplayRole, Qt.EditRole):
ans = self.dc[self.column_map[index.column()]](index.row())
return NONE if ans is None else QVariant(ans)
return self.dc[self.column_map[index.column()]](index.row())
elif role == Qt.DecorationRole:
if self.column_map[index.column()] in self.dc_decorator:
return self.dc_decorator[self.column_map[index.column()]](index.row())
return None
#elif role == Qt.SizeHintRole:
# return QVariant(Qt.SizeHint(1, 23))
#elif role == Qt.TextAlignmentRole and self.column_map[index.column()] in ('size', 'timestamp'):
# return QVariant(Qt.AlignVCenter | Qt.AlignCenter)
#elif role == Qt.ToolTipRole and index.isValid():
@ -636,6 +771,8 @@ class BooksModel(QAbstractTableModel):
return NONE
def headerData(self, section, orientation, role):
if role == Qt.ToolTipRole:
return QVariant(_('The lookup/search name is "{0}"').format(self.column_map[section]))
if role != Qt.DisplayRole:
return NONE
if orientation == Qt.Horizontal:
@ -646,55 +783,89 @@ class BooksModel(QAbstractTableModel):
def flags(self, index):
flags = QAbstractTableModel.flags(self, index)
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
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):
if role == Qt.EditRole:
row, col = index.row(), index.column()
column = self.column_map[col]
if column not in self.editable_cols:
return False
val = int(value.toInt()[0]) if column == 'rating' else \
value.toDate() if column in ('timestamp', 'pubdate') else \
unicode(value.toString())
id = self.db.id(row)
if column == 'rating':
val = 0 if val < 0 else 5 if val > 5 else val
val *= 2
self.db.set_rating(id, val)
elif column == 'series':
val = val.strip()
pat = re.compile(r'\[([.0-9]+)\]')
match = pat.search(val)
if match is not None:
self.db.set_series_index(id, float(match.group(1)))
val = pat.sub('', val).strip()
elif val:
if tweaks['series_index_auto_increment'] == 'next':
ni = self.db.get_next_series_num_for(val)
if ni != 1:
self.db.set_series_index(id, ni)
if val:
self.db.set_series(id, val)
elif column == 'timestamp':
if val.isNull() or not val.isValid():
if self.is_custom_column(column):
if not self.set_custom_column_data(row, column, value):
return False
self.db.set_timestamp(id, qt_to_dt(val, as_utc=False))
elif column == 'pubdate':
if val.isNull() or not val.isValid():
return False
self.db.set_pubdate(id, qt_to_dt(val, as_utc=False))
else:
self.db.set(row, column, val)
if column not in self.editable_cols:
return False
val = int(value.toInt()[0]) if column == 'rating' else \
value.toDate() if column in ('timestamp', 'pubdate') else \
unicode(value.toString())
id = self.db.id(row)
if column == 'rating':
val = 0 if val < 0 else 5 if val > 5 else val
val *= 2
self.db.set_rating(id, val)
elif column == 'series':
val = val.strip()
pat = re.compile(r'\[([.0-9]+)\]')
match = pat.search(val)
if match is not None:
self.db.set_series_index(id, float(match.group(1)))
val = pat.sub('', val).strip()
elif val:
if tweaks['series_index_auto_increment'] == 'next':
ni = self.db.get_next_series_num_for(val)
if ni != 1:
self.db.set_series_index(id, ni)
if val:
self.db.set_series(id, val)
elif column == 'timestamp':
if val.isNull() or not val.isValid():
return False
self.db.set_timestamp(id, qt_to_dt(val, as_utc=False))
elif column == 'pubdate':
if val.isNull() or not val.isValid():
return False
self.db.set_pubdate(id, qt_to_dt(val, as_utc=False))
else:
self.db.set(row, column, val)
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
index, index)
if column == self.sorted_on[0]:
self.resort()
return True
def set_search_restriction(self, s):
self.db.data.set_search_restriction(s)
class BooksView(TableView):
TIME_FMT = '%d %b %Y'
wrapper = textwrap.TextWrapper(width=20)
@ -711,13 +882,16 @@ class BooksView(TableView):
def __init__(self, parent, modelcls=BooksModel):
TableView.__init__(self, parent)
self.rating_delegate = LibraryDelegate(self)
self.rating_delegate = RatingDelegate(self)
self.timestamp_delegate = DateDelegate(self)
self.pubdate_delegate = PubDateDelegate(self)
self.tags_delegate = TagsDelegate(self)
self.authors_delegate = TextDelegate(self)
self.series_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._model = modelcls(self)
self.setModel(self._model)
@ -772,6 +946,25 @@ class BooksView(TableView):
self.setItemDelegateForColumn(cm.index('publisher'), self.publisher_delegate)
if 'series' in cm:
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,
save, open_folder, book_details, delete, similar_menu=None):
@ -798,6 +991,16 @@ class BooksView(TableView):
self.context_menu.popup(event.globalPos())
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):
try:
idx = self._model.column_map.index(colname)
@ -833,7 +1036,6 @@ class BooksView(TableView):
event.accept()
self.emit(SIGNAL('files_dropped(PyQt_PyObject)'), paths)
def set_database(self, db):
self._model.set_database(db)
self.tags_delegate.set_database(db)
@ -854,6 +1056,10 @@ class BooksView(TableView):
self.connect(self._model, SIGNAL('searched(PyQt_PyObject)'),
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):
QObject.connect(self._model, SIGNAL('new_bookdisplay_data(PyQt_PyObject)'),
bd)
@ -949,7 +1155,6 @@ class OnDeviceSearch(SearchQueryParser):
matches.add(index)
break
except ValueError: # Unicode errors
import traceback
traceback.print_exc()
return matches
@ -1187,5 +1392,6 @@ class DeviceBooksModel(BooksModel):
def set_editable(self, editable):
self.editable = editable
def set_search_restriction(self, s):
pass

View File

@ -306,6 +306,12 @@
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="TagsView" name="tags_view">
<property name="maximumSize">
<size>
<width>256</width>
<height>16777215</height>
</size>
</property>
<property name="tabKeyNavigation">
<bool>true</bool>
</property>
@ -328,21 +334,59 @@
</widget>
</item>
<item>
<widget class="QComboBox" name="tag_match">
<property name="currentIndex">
<number>0</number>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_3">
<item>
<property name="text">
<string>Match any</string>
</property>
<widget class="QComboBox" name="tag_match">
<property name="currentIndex">
<number>0</number>
</property>
<item>
<property name="text">
<string>Match any</string>
</property>
</item>
<item>
<property name="text">
<string>Match all</string>
</property>
</item>
</widget>
</item>
<item>
<property name="text">
<string>Match all</string>
</property>
<widget class="QPushButton" name="edit_categories">
<property name="text">
<string>Manage tag categories</string>
</property>
<property name="toolTip">
<string>Create, edit, and delete tag categories</string>
</property>
</widget>
</item>
</widget>
</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>
</item>

View File

@ -10,6 +10,7 @@ from PyQt4.Qt import QComboBox, SIGNAL, Qt, QLineEdit, QStringList, pyqtSlot
from PyQt4.QtGui import QCompleter
from calibre.gui2 import config
from calibre.gui2.dialogs.confirm_delete import confirm
class SearchLineEdit(QLineEdit):
@ -278,6 +279,10 @@ class SavedSearchBox(QComboBox):
# SIGNALed from the main UI
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
if idx < 0:
return

View File

@ -8,11 +8,13 @@ Browsing book collection by tags.
'''
from itertools import izip
from copy import copy
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
QFont, SIGNAL, QSize, QIcon, QPoint, \
QAbstractItemModel, QVariant, QModelIndex
from calibre.gui2 import config, NONE
from calibre.utils.config import prefs
from calibre.utils.search_query_parser import saved_searches
from calibre.library.database2 import Tag
@ -27,16 +29,24 @@ class TagsView(QTreeView):
self.setIconSize(QSize(30, 30))
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.popularity = popularity
self.restriction = restriction
self.tag_match = tag_match
self.db = db
self.setModel(self._model)
self.connect(self, SIGNAL('clicked(QModelIndex)'), self.toggle)
self.popularity.setChecked(config['sort_by_popularity'])
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)
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):
self.need_refresh.emit()
@ -48,6 +58,19 @@ class TagsView(QTreeView):
def sort_changed(self, state):
config.set('sort_by_popularity', state == Qt.Checked)
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):
modifiers = int(QApplication.keyboardModifiers())
@ -59,6 +82,20 @@ class TagsView(QTreeView):
def clear(self):
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):
ci = self.currentIndex()
if not ci.isValid():
@ -74,6 +111,15 @@ class TagsView(QTreeView):
self.setCurrentIndex(idx)
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):
CATEGORY = 0
@ -148,28 +194,90 @@ class TagTreeItem(object):
class TagsModel(QAbstractItemModel):
categories = [_('Authors'), _('Series'), _('Formats'), _('Publishers'), _('News'), _('Tags'), _('Searches')]
row_map = ['author', 'series', 'format', 'publisher', 'news', 'tag', 'search']
categories_orig = [_('Authors'), _('Series'), _('Formats'), _('Publishers'), _('News'), _('All tags')]
row_map_orig = ['author', 'series', 'format', 'publisher', 'news', 'tag']
fixed_categories= 5
search_keys=['search', _('Searches')]
def __init__(self, db, parent=None):
QAbstractItemModel.__init__(self, parent)
self.cmap = tuple(map(QIcon, [I('user_profile.svg'),
self.cmap_orig = list(map(QIcon, [I('user_profile.svg'),
I('series.svg'), I('book.svg'), I('publisher.png'),
I('news.svg'), I('tags.svg'), I('search.svg')]))
I('news.svg')]))
self.icon_map = [QIcon(), QIcon(I('plus.svg')),
QIcon(I('minus.svg'))]
self.db = db
self.search_restriction = ''
self.user_categories = {}
self.ignore_next_search = 0
data = self.get_node_tree(config['sort_by_popularity'])
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):
c = TagTreeItem(parent=self.root_item,
data=self.categories[i], category_icon=self.cmap[i])
for tag in data[r]:
TagTreeItem(parent=c, data=tag, icon_map=self.icon_map)
def set_search_restriction(self, s):
self.search_restriction = s
def get_node_tree(self, sort):
self.row_map = []
self.categories = []
self.cmap = self.cmap_orig[:]
self.user_categories = dict.copy(config['tag_categories'])
column_map = config['column_map']
for i in range(0, self.fixed_categories): # 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,
ids=self.db.search(self.search_restriction, return_matches=True))
else:
data = self.db.get_categories(sort_on_count=sort)
for i in data: # now the custom columns
if i not in self.row_map_orig and i in column_map:
self.row_map.append(i)
self.categories.append(self.db.custom_column_label_map[i]['name'])
self.cmap.append(QIcon(I('column.svg')))
for i in self.row_map_orig:
if i not in self.user_categories:
self.user_categories[i] = {}
config['tag_categories'] = self.user_categories
taglist = {} # Now the user-defined categories
for i in data:
taglist[i] = dict(map(lambda t:(t.name if i != 'author' else t.name.replace('|', ','), t), data[i]))
for k in self.row_map_orig:
if k not in self.user_categories:
continue
for i in sorted(self.user_categories[k].keys()): # now the tag categories
l = []
for t in self.user_categories[k][i]:
if t in taglist[k]: # use same tag node as the complete category
l.append(taglist[k][t])
# else: eliminate nodes that have zero counts
data[i+'*'] = l
self.row_map.append(i+'*')
self.categories.append(i)
if k == 'tag': # choose the icon
self.cmap.append(QIcon(I('tags.svg')))
else:
self.cmap.append(QIcon(self.cmap[self.row_map_orig.index(k)]))
# Now the rest of the normal tag categories
for i in range(self.fixed_categories, len(self.row_map_orig)):
self.row_map.append(self.row_map_orig[i])
self.categories.append(self.categories_orig[i])
self.cmap.append(QIcon(I('tags.svg')))
data['search'] = self.get_search_nodes() # Add the search category
self.row_map.append(self.search_keys[0])
self.categories.append(self.search_keys[1])
self.cmap.append(QIcon(I('search.svg')))
return data
def get_search_nodes(self):
l = []
@ -178,8 +286,7 @@ class TagsModel(QAbstractItemModel):
return l
def refresh(self):
data = self.db.get_categories(config['sort_by_popularity'])
data['search'] = self.get_search_nodes()
data = self.get_node_tree(config['sort_by_popularity']) # get category data
for i, r in enumerate(self.row_map):
category = self.root_item.children[i]
names = [t.tag.name for t in category.children]
@ -194,8 +301,6 @@ class TagsModel(QAbstractItemModel):
if len(data[r]) > 0:
self.beginInsertRows(category_index, 0, len(data[r])-1)
for tag in data[r]:
if r == 'author':
tag.name = tag.name.replace('|', ',')
tag.state = state_map.get(tag.name, 0)
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_map)
self.endInsertRows()
@ -273,16 +378,20 @@ class TagsModel(QAbstractItemModel):
return len(parent_item.children)
def reset_all_states(self, except_=None):
update_list = []
for i in xrange(self.rowCount(QModelIndex())):
category_index = self.index(i, 0, QModelIndex())
for j in xrange(self.rowCount(category_index)):
tag_index = self.index(j, 0, category_index)
tag_item = tag_index.internalPointer()
if tag_item is except_:
continue
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
update_list.append(tag)
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
tag_index, tag_index)
@ -299,9 +408,9 @@ class TagsModel(QAbstractItemModel):
if not index.isValid(): return False
item = index.internalPointer()
if item.type == TagTreeItem.TAG:
if exclusive:
self.reset_all_states(except_=item)
item.toggle()
if exclusive:
self.reset_all_states(except_=item.tag)
self.ignore_next_search = 2
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), index, index)
return True
@ -309,14 +418,19 @@ class TagsModel(QAbstractItemModel):
def tokens(self):
ans = []
tags_seen = []
for i, key in enumerate(self.row_map):
category_item = self.root_item.children[i]
for tag_item in category_item.children:
tag = tag_item.tag
category = key if key != 'news' else 'tag'
if tag.state > 0:
prefix = ' not ' if tag.state == 2 else ''
category = key if not key.endswith('*') and \
key not in ['news', 'specialtags', 'normaltags'] \
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))
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.caches import CoverCache
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.tag_categories import TagCategories
from datetime import datetime
class SaveMenu(QMenu):
@ -126,8 +129,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
pixmap_to_data(pixmap))
def __init__(self, listener, opts, actions, parent=None):
self.last_time = datetime.now()
self.preferences_action, self.quit_action = actions
self.spare_servers = []
self.must_restart_before_config = False
MainWindow.__init__(self, opts, parent)
# Initialize fontconfig in a separate thread as this can be a lengthy
# process if run for the first time on this machine
@ -143,6 +148,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.setupUi(self)
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,
help_text=_('Search (For Advanced Search click the button to the left)'))
self.connect(self.clear_button, SIGNAL('clicked()'), self.search_clear)
@ -320,7 +328,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
QObject.connect(md.actions()[7], SIGNAL('triggered(bool)'),
self.__em6__)
self.save_menu = QMenu()
self.save_menu.addAction(_('Save to disk'))
self.save_menu.addAction(_('Save to disk in a single directory'))
@ -475,6 +482,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.search_done)),
('connect_to_book_display',
(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):
getattr(view, func)(*args)
@ -514,8 +523,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
db = LibraryDatabase2(self.library_path)
self.library_view.set_database(db)
prefs['library_path'] = self.library_path
self.library_view.sortByColumn(*dynamic.get('sort_column',
('timestamp', Qt.DescendingOrder)))
self.library_view.restore_sort_at_startup(dynamic.get('sort_history', [('timestamp', Qt.DescendingOrder)]))
if not self.library_view.restore_column_widths():
self.library_view.resizeColumnsToContents()
self.search.setFocus(Qt.OtherFocusReason)
@ -525,10 +533,20 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.tags_view.setVisible(False)
self.tag_match.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,
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
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,
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
self.saved_search.clear_to_help)
@ -541,8 +559,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
SIGNAL('count_changed(int)'), self.location_view.count_changed)
self.connect(self.library_view.model(), SIGNAL('count_changed(int)'),
self.tags_view.recount, Qt.QueuedConnection)
self.connect(self.search, SIGNAL('cleared()'), self.tags_view_clear)
self.connect(self.saved_search, SIGNAL('changed()'), self.tags_view.recount, Qt.QueuedConnection)
self.connect(self.library_view.model(), SIGNAL('count_changed(int)'),
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):
from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
@ -592,7 +612,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.resize(self.width(), self._calculated_available_height)
self.search.setMaximumWidth(self.width()-150)
if config['autolaunch_server']:
from calibre.library.server import start_threaded_server
from calibre.library import server_config
@ -632,6 +651,12 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
height = v.rowHeight(0)
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):
MainWindow.resizeEvent(self, ev)
@ -783,23 +808,68 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.tags_view.setVisible(True)
self.tag_match.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)
else:
self.tags_view.setVisible(False)
self.tag_match.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.saved_search.clear_to_help()
def search_clear(self):
self.search_count.setText(_("(all books)"))
self.set_number_of_books_shown(all='yes', compute_count=True)
self.search.clear()
def search_done(self, view, ok):
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)
def sync_cf_to_listview(self, current, previous):
@ -2028,7 +2098,12 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
_('Cannot configure while there are running jobs.'))
d.exec_()
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)
d.exec_()
self.content_server = d.server
@ -2043,15 +2118,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
_('Save only %s format to disk')%
prefs['output_format'].upper())
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()
if not patheq(self.library_path, d.database_location):
newloc = d.database_location
move_library(self.library_path, newloc, self,
self.library_moved)
def library_moved(self, newloc):
if newloc is None: return
db = LibraryDatabase2(newloc)
@ -2226,7 +2303,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
def write_settings(self):
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('cover_flow_visible', self.cover_flow.isVisible())
self.library_view.write_settings()

View File

@ -12,8 +12,9 @@ from itertools import repeat
from PyQt4.QtCore import QThread, QReadWriteLock
from PyQt4.QtGui import QImage
from calibre.utils.search_query_parser import SearchQueryParser
from calibre.utils.config import tweaks, prefs
from calibre.utils.date import parse_date
from calibre.utils.search_query_parser import SearchQueryParser
class CoverCache(QThread):
@ -146,6 +147,14 @@ class ResultCache(SearchQueryParser):
'''
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):
'''
@ -194,13 +203,6 @@ class ResultCache(SearchQueryParser):
self.search_relops = {'=':[1, relop_eq], '>':[1, relop_gt], '<':[1, relop_lt], \
'!=':[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):
return self._data[self._map_filtered[row]]
@ -214,30 +216,45 @@ class ResultCache(SearchQueryParser):
def universal_set(self):
return set([i[0] for i in self._data if i is not None])
def get_matches_dates(self, location, query):
matches = set([])
if len(query) < 2:
return matches
relop = None
for k in self.search_relops.keys():
if query.startswith(k):
(p, relop) = self.search_relops[k]
query = query[p:]
if relop is None:
(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]]
try:
qd = parse_date(query)
except:
raise ParseException(query, len(query), 'Date conversion error', self)
if '-' in query:
field_count = query.count('-') + 1
else:
field_count = query.count('/') + 1
for item in self._data:
if item is None or item[loc] is None: continue
if relop(item[loc], qd, field_count):
matches.add(item[0])
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'):
if len(query) < 2:
return matches
relop = None
for k in self.search_relops.keys():
if query.startswith(k):
(p, relop) = self.search_relops[k]
query = query[p:]
if relop is None:
return matches
loc = self.FIELD_MAP[{'date':'timestamp', 'pubdate':'pubdate'}[location]]
qd = parse_date(query)
field_count = query.count('-') + 1
for item in self._data:
if item is None: continue
if relop(item[loc], qd, field_count):
matches.add(item[0])
return matches
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
matchkind = CONTAINS_MATCH
@ -257,19 +274,38 @@ class ResultCache(SearchQueryParser):
query = query.decode('utf-8')
if location in ('tag', 'author', 'format', 'comment'):
location += 's'
all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series', 'formats', 'isbn', 'rating', 'cover')
MAP = {}
for x in all:
for x in all: # get the db columns for the standard searchables
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('')
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']]
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())
for i, loc in enumerate(location):
location[i] = MAP[loc]
try:
rating_query = int(query) * 2
except:
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:
if loc == MAP['authors']:
q = query.replace(',', '|'); ### DB stores authors with commas changed to bars, so change query
@ -278,14 +314,34 @@ class ResultCache(SearchQueryParser):
for item in self._data:
if item is None: continue
if IS_CUSTOM[loc] == 'bool': # complexity caused by the two-/three-value tweak
v = item[loc]
if not bools_are_tristate:
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])
continue
if not item[loc]:
if query == 'false':
if isinstance(item[loc], basestring):
if item[loc].strip() != '':
continue
if q == 'false':
matches.add(item[0])
continue
continue ### item is empty. No possible matches below
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 isinstance(item[loc], basestring):
@ -293,12 +349,35 @@ class ResultCache(SearchQueryParser):
continue
matches.add(item[0])
continue
if rating_query and loc == MAP['rating'] and rating_query == int(item[loc]):
matches.add(item[0])
if rating_query:
if (loc == MAP['rating'] and rating_query == int(item[loc])):
matches.add(item[0])
continue
if IS_CUSTOM[loc] == 'rating':
if rating_query and rating_query == int(item[loc]):
matches.add(item[0])
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 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:
vals = [item[loc]] ### make into list to make _match happy
if _match(q, vals, matchkind):
@ -342,8 +421,7 @@ class ResultCache(SearchQueryParser):
'''
for id in ids:
try:
self._data[id] = db.conn.get('SELECT * from meta2 WHERE id=?',
(id,))[0]
self._data[id] = db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0]
self._data[id].append(db.has_cover(id, index_is_id=True))
except IndexError:
return None
@ -399,6 +477,12 @@ class ResultCache(SearchQueryParser):
asstr else cmp(self._data[x][loc], self._data[y][loc])
except AttributeError: # Some entries may be None
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:
return cmp(self._data[x][11].lower(), self._data[y][11].lower())
return ans
@ -410,21 +494,35 @@ class ResultCache(SearchQueryParser):
if field == 'date': field = 'timestamp'
elif field == 'title': field = '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:
subsort = True
self.first_sort = False
fcmp = self.seriescmp if field == 'series' else \
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_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():
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)
return
matches = sorted(self.parse(query))
return []
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]
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))
changed = True
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))
self.custom_column_num_map[num]['is_editable'] = bool(is_editable)
changed = True

View File

@ -56,8 +56,6 @@ def delete_tree(path, permanent=False):
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
class Tag(object):
def __init__(self, name, id=None, count=0, state=0, tooltip=None):
@ -186,7 +184,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.executescript(script)
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.refresh = functools.partial(self.data.refresh, self)
self.sort = self.data.sort
@ -576,35 +574,98 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def get_recipe(self, id):
return self.conn.get('SELECT script FROM feeds WHERE id=?', (id,), all=False)
def get_categories(self, sort_on_count=False):
self.conn.executescript(u'''
CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT
id,
name,
(SELECT COUNT(id) FROM books_tags_link WHERE tag=x.id) 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()
def get_categories(self, sort_on_count=False, ids=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'''
CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT
id,
name,
(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
(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()
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 = {}
for x in ('tags', 'series', 'news', 'publishers', 'authors'):
query = 'SELECT id,name,count FROM tag_browser_'+x
for tn,cn in cat_cols.iteritems():
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:
query += ' ORDER BY count DESC'
else:
query += ' ORDER BY name ASC'
query += ' ORDER BY {0} ASC'.format(cn[1])
data = self.conn.get(query)
category = x if x in ('series', 'news') else x[:-1]
categories[category] = [Tag(r[1], count=r[2], id=r[0]) for r in data]
category = cn[0]
if ids is None: # no filtering
categories[category] = [Tag(r[1], count=r[2], id=r[0])
for r in data]
else: # filter out zero-count tags
categories[category] = [Tag(r[1], count=r[2], id=r[0])
for r in data if r[2] > 0]
categories['format'] = []
for fmt in self.conn.get('SELECT DISTINCT format FROM data'):
fmt = fmt[0]
count = self.conn.get('SELECT COUNT(id) FROM data WHERE format="%s"'%fmt,
all=False)
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)
categories['format'].append(Tag(fmt, count=count))
if sort_on_count:
@ -612,7 +673,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
reverse=True)
else:
categories['format'].sort(cmp=lambda x,y:cmp(x.name, y.name))
return categories
def tags_older_than(self, tag, delta):

View File

@ -116,13 +116,12 @@ class SearchQueryParser(object):
failed.append(test[0])
return failed
def __init__(self, test=False):
def __init__(self, custcols=[], test=False):
self._tests_failed = False
# Define a token
locations = map(lambda x : CaselessLiteral(x)+Suppress(':'),
self.LOCATIONS)
standard_locations = map(lambda x : CaselessLiteral(x)+Suppress(':'), self.LOCATIONS+custcols)
location = NoMatch()
for l in locations:
for l in standard_locations:
location |= l
location = Optional(location, default='all')
word_query = CharsNotIn(string.whitespace + '()')
@ -176,14 +175,20 @@ class SearchQueryParser(object):
def parse(self, query):
# empty the list of searches used for recursion testing
self.recurse_level = 0
self.searches_seen = set([])
return self._parse(query)
# 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):
self.recurse_level += 1
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):
return getattr(self, 'evaluate_'+group_name)
@ -213,7 +218,8 @@ class SearchQueryParser(object):
try:
if query in self.searches_seen:
raise ParseException(query, len(query), 'undefined saved search', self)
self.searches_seen.add(query)
if self.recurse_level > 5:
self.searches_seen.add(query)
return self._parse(saved_searches.lookup(query))
except: # convert all exceptions (e.g., missing key) to a parse error
raise ParseException(query, len(query), 'undefined saved search', self)