Original CC code, prepare for merge with beta tree

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

View File

@ -25,3 +25,20 @@ series_index_auto_increment = 'next'
# copy : copy author to author_sort without modification
# 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

2679
resources/images/drawer.svg Normal file

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 278 KiB

View File

@ -13,6 +13,7 @@ from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
ORG_NAME = 'KovidsBrain'
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,8 @@ 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('user_categories', default={},
help=_('User-created tag browser 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 %s is already used')%col)
return
bad_head = False
for t in self.parent.custcols:
if self.parent.custcols[t]['name'] == col_heading:
if not self.editing_col or self.parent.custcols[t]['num'] != self.orig_column_number:
bad_head = True
for t in self.standard_colheads:
if self.standard_colheads[t] == col_heading:
bad_head = True
if bad_head:
self.parent.messagebox(_('The heading %s is already used')%col_heading)
return
if col.find(':') >= 0 or col.find(' ') >= 0 and \
(not is_alpha(col) or is_lower(col)):
self.parent.messagebox(_('The lookup name must be lower case and cannot contain ":"s or spaces'))
return
if not self.editing_col:
self.parent.custcols[col] = {
'label':col,
'name':col_heading,
'datatype':col_type,
'editable':True,
'display':None,
'normalized':None,
'num':None,
'is_multiple':is_multiple,
}
item = QListWidgetItem(col_heading, self.parent.columns)
item.setData(Qt.UserRole, QVariant(col))
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
item.setCheckState(Qt.Checked)
else:
idx = self.parent.columns.currentRow()
item = self.parent.columns.item(idx)
item.setData(Qt.UserRole, QVariant(col))
item.setText(col_heading)
self.parent.custcols[self.orig_column_name]['label'] = col
self.parent.custcols[self.orig_column_name]['name'] = col_heading
self.parent.custcols[self.orig_column_name]['*edited'] = True
self.parent.custcols[self.orig_column_name]['*must_restart'] = True
QDialog.accept(self)
def reject(self):
QDialog.reject(self)

View File

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

View File

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

View File

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

View File

@ -1,32 +1,34 @@
__license__ = 'GPL v3'
__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, \
QPainterPath, QLinearGradient, QBrush, \
QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
QImage, QMenu, \
QStyledItemDelegate, QCompleter
QIcon, QImage, QMenu, \
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(QStyledItemDelegate):
COLOR = QColor("blue")
@ -98,7 +100,6 @@ class LibraryDelegate(QStyledItemDelegate):
return sb
class DateDelegate(QStyledItemDelegate):
def displayText(self, val, locale):
d = val.toDate()
return d.toString('dd MMM yyyy')
@ -114,7 +115,6 @@ class DateDelegate(QStyledItemDelegate):
return qde
class PubDateDelegate(QStyledItemDelegate):
def displayText(self, val, locale):
return val.toDate().toString('MMM yyyy')
@ -126,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
@ -150,7 +149,6 @@ class TextDelegate(QStyledItemDelegate):
return editor
class TagsDelegate(QStyledItemDelegate):
def __init__(self, parent):
QStyledItemDelegate.__init__(self, parent)
self.db = None
@ -160,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)"),
@ -185,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:
@ -201,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.build_data_convertors()
self.reset()
self.emit(SIGNAL('columns_sorted()'))
def set_database(self, db):
self.db = db
self.build_data_convertors()
self.custom_columns = self.db.custom_column_label_map
self.read_config()
def refresh_ids(self, ids, current_row=-1):
rows = self.db.refresh_ids(ids)
@ -316,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):
@ -323,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:
@ -430,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]
@ -562,75 +661,119 @@ 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, bool_cols_are_tristate=True):
val = self.db.data[r][idx]
if not bool_cols_are_tristate:
if val is None or not val:
return self.bool_no_icon
if val:
return self.bool_yes_icon
if val is None:
return self.bool_blank_icon
return self.bool_no_icon
def text_type(r, mult=False, idx=-1):
text = self.db.data[r][idx]
if text and mult:
return QVariant(', '.join(sorted(text.split('|'))))
return QVariant(text)
def number_type(r, idx=-1):
return QVariant(self.db.data[r][idx])
self.dc = {
'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,
bool_cols_are_tristate=tweaks['bool_custom_columns_are_tristate'] == 'yes')
elif datatype == 'rating':
self.dc[col] = functools.partial(rating_type, idx=idx)
else:
print 'What type is this?', col, datatype
# build a index column to data converter map, to remove the string lookup in the data loop
self.column_to_dc_map = []
self.column_to_dc_decorator_map = []
for col in self.column_map:
self.column_to_dc_map.append(self.dc[col])
self.column_to_dc_decorator_map.append(self.dc_decorator.get(col, None))
def data(self, index, role):
col = index.column()
# in obscure cases where custom columns are both edited and added, for a time
# the column map does not accurately represent the screen. In these cases,
# we will get asked to display columns we don't know about. Must test for this.
if col >= len(self.column_to_dc_map):
return None
if role in (Qt.DisplayRole, Qt.EditRole):
ans = self.dc[self.column_map[index.column()]](index.row())
return NONE if ans is None else QVariant(ans)
return self.column_to_dc_map[col](index.row())
elif role == Qt.DecorationRole:
if self.column_to_dc_decorator_map[col] is not None:
return self.column_to_dc_decorator_map[index.column()](index.row())
#elif role == Qt.TextAlignmentRole and self.column_map[index.column()] in ('size', 'timestamp'):
# return QVariant(Qt.AlignVCenter | Qt.AlignCenter)
#elif role == Qt.ToolTipRole and index.isValid():
@ -639,65 +782,105 @@ class BooksModel(QAbstractTableModel):
return NONE
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
if orientation == Qt.Horizontal:
return QVariant(self.headers[self.column_map[section]])
else:
if section >= len(self.column_map): # same problem as in data, the column_map can be wrong
return None
if role == Qt.ToolTipRole:
return QVariant(_('The lookup/search name is "{0}"').format(self.column_map[section]))
if role == Qt.DisplayRole:
return QVariant(self.headers[self.column_map[section]])
return NONE
if role == Qt.DisplayRole: # orientation is vertical
return QVariant(section+1)
return NONE
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)
@ -721,6 +904,9 @@ class BooksView(TableView):
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)
@ -775,6 +961,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):
@ -801,6 +1006,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)
@ -836,7 +1051,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)
@ -857,6 +1071,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)
@ -952,7 +1170,6 @@ class OnDeviceSearch(SearchQueryParser):
matches.add(index)
break
except ValueError: # Unicode errors
import traceback
traceback.print_exc()
return matches
@ -1190,5 +1407,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 user categories</string>
</property>
<property name="toolTip">
<string>Create, edit, and delete user 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):
@ -226,7 +227,6 @@ class SavedSearchBox(QComboBox):
self.clear_to_help()
def normalize_state(self):
#print 'in normalize_state'
self.setEditText('')
self.line_edit.setStyleSheet(
'QLineEdit { color: black; background-color: %s; }' %
@ -234,7 +234,6 @@ class SavedSearchBox(QComboBox):
self.help_state = False
def clear_to_help(self):
#print 'in clear_to_help'
self.setToolTip(self.tool_tip_text)
self.initialize_saved_search_names()
self.setEditText(self.help_text)
@ -245,12 +244,10 @@ class SavedSearchBox(QComboBox):
self.normal_background)
def focus_out(self, event):
#print 'in focus_out'
if self.currentText() == '':
self.clear_to_help()
def key_pressed(self, event):
#print 'in key_pressed'
if self.help_state:
self.normalize_state()
@ -259,7 +256,6 @@ class SavedSearchBox(QComboBox):
self.normalize_state()
def saved_search_selected (self, qname):
#print 'in saved_search_selected'
qname = unicode(qname)
if qname is None or not qname.strip():
return
@ -269,7 +265,6 @@ class SavedSearchBox(QComboBox):
self.setToolTip(self.saved_searches.lookup(qname))
def initialize_saved_search_names(self):
#print 'in initialize_saved_search_names'
self.clear()
qnames = self.saved_searches.names()
self.addItems(qnames)
@ -277,7 +272,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
@ -288,7 +286,6 @@ class SavedSearchBox(QComboBox):
# SIGNALed from the main UI
def save_search_button_clicked(self):
#print 'in save_search_button_clicked'
name = unicode(self.currentText())
if self.help_state or not name.strip():
name = unicode(self.search_box.text()).replace('"', '')
@ -305,10 +302,7 @@ class SavedSearchBox(QComboBox):
# SIGNALed from the main UI
def copy_search_button_clicked (self):
#print 'in copy_search_button_clicked'
idx = self.currentIndex();
if idx < 0:
return
self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText())))
self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText())))

View File

@ -8,11 +8,13 @@ Browsing book collection by tags.
'''
from itertools import izip
from 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,13 +111,22 @@ 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
TAG = 1
ROOT = 2
def __init__(self, data=None, tag=None, category_icon=None, icon_map=None, parent=None):
def __init__(self, data=None, category_icon=None, icon_map=None, parent=None):
self.parent = parent
self.children = []
if self.parent is not None:
@ -96,13 +142,14 @@ class TagTreeItem(object):
self.bold_font.setBold(True)
self.bold_font = QVariant(self.bold_font)
elif self.type == self.TAG:
self.tag, self.icon_map = data, list(map(QVariant, icon_map))
icon_map[0] = data.icon
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
def __str__(self):
if self.type == self.ROOT:
return 'ROOT'
if self.type == self.CATEGORY:
return 'CATEGORY:'+self.name+':%d'%len(self.children)
return 'CATEGORY:'+str(QVariant.toString(self.name))+':%d'%len(self.children)
return 'TAG:'+self.tag.name
def row(self):
@ -137,7 +184,7 @@ class TagTreeItem(object):
else:
return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
if role == Qt.DecorationRole:
return self.icon_map[self.tag.state]
return self.icon_state_map[self.tag.state]
if role == Qt.ToolTipRole and self.tag.tooltip:
return QVariant(self.tag.tooltip)
return NONE
@ -148,38 +195,100 @@ 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']
tags_categories_start= 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.cat_icon_map_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')]))
self.icon_map = [QIcon(), QIcon(I('plus.svg')),
QIcon(I('minus.svg'))]
I('news.svg'), I('tags.svg')]))
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
self.custcol_icon = QIcon(I('column.svg'))
self.search_icon = QIcon(I('search.svg'))
self.usercat_icon = QIcon(I('drawer.svg'))
self.label_to_icon_map = dict(map(None, self.row_map_orig, self.cat_icon_map_orig))
self.label_to_icon_map['*custom'] = self.custcol_icon
self.db = db
self.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])
data=self.categories[i], category_icon=self.cat_icon_map[i])
for tag in data[r]:
TagTreeItem(parent=c, data=tag, icon_map=self.icon_map)
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
def set_search_restriction(self, s):
self.search_restriction = s
def get_search_nodes(self):
def get_node_tree(self, sort):
self.row_map = []
self.categories = []
# strip the icons after the 'standard' categories. We will put them back later
self.cat_icon_map = self.cat_icon_map_orig[:self.tags_categories_start-len(self.row_map_orig)]
self.user_categories = dict.copy(config['user_categories'])
column_map = config['column_map']
for i in range(0, self.tags_categories_start): # First the standard categories
self.row_map.append(self.row_map_orig[i])
self.categories.append(self.categories_orig[i])
if len(self.search_restriction):
data = self.db.get_categories(sort_on_count=sort, icon_map=self.label_to_icon_map,
ids=self.db.search(self.search_restriction, return_matches=True))
else:
data = self.db.get_categories(sort_on_count=sort, icon_map=self.label_to_icon_map)
for c in data: # now the custom columns
if c not in self.row_map_orig and c in column_map:
self.row_map.append(c)
self.categories.append(self.db.custom_column_label_map[c]['name'])
self.cat_icon_map.append(self.custcol_icon)
# Now do the user-defined categories. There is a time/space tradeoff here.
# By converting the tags into a map, we can do the verification in the category
# loop much faster, at the cost of duplicating the categories lists.
taglist = {}
for c in self.row_map_orig:
taglist[c] = dict(map(lambda t:(t.name if c != 'author' else t.name.replace('|', ','), t), data[c]))
for c in self.user_categories:
l = []
for (name,label,ign) in self.user_categories[c]:
if name in taglist[label]: # use same node as the complete category
l.append(taglist[label][name])
# else: do nothing, to eliminate nodes that have zero counts
if config['sort_by_popularity']:
data[c+'*'] = sorted(l, cmp=(lambda x, y: cmp(x.count, y.count)))
else:
data[c+'*'] = sorted(l, cmp=(lambda x, y: cmp(x.name.lower(), y.name.lower())))
self.row_map.append(c+'*')
self.categories.append(c)
self.cat_icon_map.append(self.usercat_icon)
# Now the rest of the normal tag categories
for i in range(self.tags_categories_start, len(self.row_map_orig)):
self.row_map.append(self.row_map_orig[i])
self.categories.append(self.categories_orig[i])
self.cat_icon_map.append(self.cat_icon_map_orig[i])
data['search'] = self.get_search_nodes(self.search_icon) # Add the search category
self.row_map.append(self.search_keys[0])
self.categories.append(self.search_keys[1])
self.cat_icon_map.append(self.search_icon)
return data
def get_search_nodes(self, icon):
l = []
for i in saved_searches.names():
l.append(Tag(i, tooltip=saved_searches.lookup(i)))
l.append(Tag(i, tooltip=saved_searches.lookup(i), icon=icon))
return l
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,10 +303,8 @@ 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)
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
self.endInsertRows()
def columnCount(self, parent):
@ -273,16 +380,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 +410,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 +420,19 @@ class TagsModel(QAbstractItemModel):
def tokens(self):
ans = []
tags_seen = []
for i, key in enumerate(self.row_map):
if key.endswith('*'): # User category, so skip it. The tag will be marked in its real category
continue
category_item = self.root_item.children[i]
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 key != 'news' else 'tag'
if category == 'tag':
if tag.name in tags_seen:
continue
tags_seen.append(tag.name)
ans.append('%s%s:"=%s"'%(prefix, category, tag.name))
return ans
return ans

View File

@ -60,6 +60,9 @@ from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
from calibre.library.database2 import LibraryDatabase2
from calibre.library.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)
@ -501,6 +509,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)
@ -540,8 +550,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)
@ -551,10 +560,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)
@ -567,8 +586,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'])
@ -618,7 +639,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
@ -658,6 +678,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)
@ -809,23 +835,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):
@ -2174,7 +2245,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
@ -2189,15 +2265,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)
@ -2374,7 +2452,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

@ -8,12 +8,15 @@ __docformat__ = 'restructuredtext en'
import collections, glob, os, re, itertools, functools
from itertools import repeat
from datetime import timedelta
from PyQt4.QtCore import QThread, QReadWriteLock
from PyQt4.QtGui import QImage
from calibre.utils.config import tweaks, prefs
from calibre.utils.date import parse_date, now
from calibre.utils.search_query_parser import SearchQueryParser
from calibre.utils.date import parse_date
from calibre.utils.pyparsing import ParseException
class CoverCache(QThread):
@ -146,6 +149,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 +205,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 +218,63 @@ 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]]
if query == _('today'):
qd = now()
field_count = 3
elif query == _('yesterday'):
qd = now() - timedelta(1)
field_count = 3
elif query == _('thismonth'):
qd = now()
field_count = 2
elif query.endswith(_('daysago')):
num = query[0:-len(_('daysago'))]
try:
qd = now() - timedelta(int(num))
except:
raise ParseException(query, len(query), 'Number conversion error', self)
field_count = 3
else:
try:
qd = parse_date(query)
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 +294,39 @@ 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('')
IS_CUSTOM[self.FIELD_MAP['rating']] = 'rating' # normal and custom ratings columns use the same code
for x in self.custom_column_label_map: # add custom columns to MAP. Put the column's type into IS_CUSTOM
if self.custom_column_label_map[x]['datatype'] != "datetime":
MAP[x] = self.FIELD_MAP[self.custom_column_label_map[x]['num']]
IS_CUSTOM[MAP[x]] = self.custom_column_label_map[x]['datatype']
EXCLUDE_FIELDS = [MAP['rating'], MAP['cover']]
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 +335,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 +370,30 @@ 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 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 +437,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 +493,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 +510,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,16 +56,15 @@ 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):
def __init__(self, name, id=None, count=0, state=0, tooltip=None, icon=None):
self.name = name
self.id = id
self.count = count
self.state = state
self.tooltip = tooltip
self.icon = icon
def __unicode__(self):
return u'%s:%s:%s:%s:%s'%(self.name, self.count, self.id, self.state, self.tooltip)
@ -186,7 +185,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.conn.executescript(script)
self.conn.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 +575,99 @@ 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, icon_map=None):
orig_category_columns = {'tags': ['tag', 'name'],
'series': ['series', 'name'],
'publishers': ['publisher', 'name'],
'authors': ['author', 'name']} # 'news' is added below
cat_cols = {}
def create_filtered_views(self, ids):
def create_tag_browser_view(table_name, column_name, view_column_name):
script = ('''
CREATE TEMP VIEW IF NOT EXISTS tag_browser_filtered_{tn} AS SELECT
id,
{vcn},
(SELECT COUNT(books_{tn}_link.id) FROM books_{tn}_link WHERE {cn}={tn}.id and books_list_filter(book)) count
FROM {tn};
'''.format(tn=table_name, cn=column_name, vcn=view_column_name))
self.conn.executescript(script)
self.cat_cols = {}
for tn,cn in orig_category_columns.iteritems():
create_tag_browser_view(tn, cn[0], cn[1])
cat_cols[tn] = cn
for i,v in self.custom_column_num_map.iteritems():
if v['datatype'] == 'text':
tn = 'custom_column_{0}'.format(i)
create_tag_browser_view(tn, 'value', 'value')
cat_cols[tn] = [v['label'], 'value']
cat_cols['news'] = ['news', 'name']
self.conn.executescript(u'''
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]
icon = icon_map[category] if category in icon_map else icon_map['*custom']
if ids is None: # no filtering
categories[category] = [Tag(r[1], count=r[2], id=r[0], icon=icon)
for r in data]
else: # filter out zero-count tags
categories[category] = [Tag(r[1], count=r[2], id=r[0], icon=icon)
for r in data if r[2] > 0]
categories['format'] = []
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 +675,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

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

View File

@ -116,13 +116,12 @@ class SearchQueryParser(object):
failed.append(test[0])
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)
@ -207,13 +212,13 @@ class SearchQueryParser(object):
location = argument[0]
query = argument[1]
if location.lower() == 'search':
# print "looking for named search " + query
if query.startswith('='):
query = query[1:]
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)