mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-07 10:14:46 -04:00
Original CC code, prepare for merge with beta tree
This commit is contained in:
commit
e7be95be7e
@ -25,3 +25,20 @@ series_index_auto_increment = 'next'
|
|||||||
# copy : copy author to author_sort without modification
|
# copy : copy author to author_sort without modification
|
||||||
# comma : use 'copy' if there is a ',' in the name, otherwise use 'invert'
|
# comma : use 'copy' if there is a ',' in the name, otherwise use 'invert'
|
||||||
author_sort_copy_method = 'invert'
|
author_sort_copy_method = 'invert'
|
||||||
|
|
||||||
|
|
||||||
|
# Set whether boolean custom columns are two- or three-valued.
|
||||||
|
# Two-values for true booleans
|
||||||
|
# three-values for yes/no/unknown
|
||||||
|
# Set to 'yes' for three-values, 'no' for two-values
|
||||||
|
bool_custom_columns_are_tristate = 'yes'
|
||||||
|
|
||||||
|
|
||||||
|
# Provide a set of columns to be sorted on when calibre starts
|
||||||
|
# The argument is None of saved sort history is to be used
|
||||||
|
# otherwise it is a list of column,order pairs. Column is the
|
||||||
|
# lookup/search name, found using the tooltip for the column
|
||||||
|
# Order is 0 for ascending, 1 for descending
|
||||||
|
# For example, set it to [('authors',0),('title',0)] to sort by
|
||||||
|
# title within authors.
|
||||||
|
sort_columns_at_startup = None
|
63
resources/images/blank.svg
Normal file
63
resources/images/blank.svg
Normal 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 |
61
resources/images/column.svg
Normal file
61
resources/images/column.svg
Normal 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
2679
resources/images/drawer.svg
Normal file
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 278 KiB |
@ -13,6 +13,7 @@ from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
|
|||||||
ORG_NAME = 'KovidsBrain'
|
ORG_NAME = 'KovidsBrain'
|
||||||
APP_UID = 'libprs500'
|
APP_UID = 'libprs500'
|
||||||
from calibre import islinux, iswindows, isosx, isfreebsd
|
from calibre import islinux, iswindows, isosx, isfreebsd
|
||||||
|
from calibre.constants import preferred_encoding
|
||||||
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
|
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
|
||||||
from calibre.utils.localization import set_qt_translator
|
from calibre.utils.localization import set_qt_translator
|
||||||
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
|
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
|
||||||
@ -95,6 +96,8 @@ def _config():
|
|||||||
help=_('Overwrite author and title with new metadata'))
|
help=_('Overwrite author and title with new metadata'))
|
||||||
c.add_opt('enforce_cpu_limit', default=True,
|
c.add_opt('enforce_cpu_limit', default=True,
|
||||||
help=_('Limit max simultaneous jobs to number of CPUs'))
|
help=_('Limit max simultaneous jobs to number of CPUs'))
|
||||||
|
c.add_opt('user_categories', default={},
|
||||||
|
help=_('User-created tag browser categories'))
|
||||||
|
|
||||||
return ConfigProxy(c)
|
return ConfigProxy(c)
|
||||||
|
|
||||||
|
17
src/calibre/gui2/dialogs/comments_dialog.py
Normal file
17
src/calibre/gui2/dialogs/comments_dialog.py
Normal 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)
|
83
src/calibre/gui2/dialogs/comments_dialog.ui
Normal file
83
src/calibre/gui2/dialogs/comments_dialog.ui
Normal 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>
|
@ -1,6 +1,6 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
import os, re, time, textwrap
|
import os, re, time, textwrap, sys, copy
|
||||||
|
|
||||||
from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
||||||
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
|
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
|
||||||
@ -8,10 +8,11 @@ from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
|||||||
SIGNAL, QThread, Qt, QSize, QVariant, QUrl, \
|
SIGNAL, QThread, Qt, QSize, QVariant, QUrl, \
|
||||||
QModelIndex, QAbstractTableModel, \
|
QModelIndex, QAbstractTableModel, \
|
||||||
QDialogButtonBox, QTabWidget, QBrush, QLineEdit, \
|
QDialogButtonBox, QTabWidget, QBrush, QLineEdit, \
|
||||||
QProgressDialog
|
QProgressDialog, QMessageBox
|
||||||
|
|
||||||
from calibre.constants import iswindows, isosx
|
from calibre.constants import iswindows, isosx, preferred_encoding
|
||||||
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
|
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
|
||||||
|
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
|
||||||
from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, \
|
from calibre.gui2 import qstring_to_unicode, choose_dir, error_dialog, config, \
|
||||||
ALL_COLUMNS, NONE, info_dialog, choose_files, \
|
ALL_COLUMNS, NONE, info_dialog, choose_files, \
|
||||||
warning_dialog, ResizableDialog
|
warning_dialog, ResizableDialog
|
||||||
@ -90,7 +91,6 @@ class ConfigTabs(QTabWidget):
|
|||||||
widget.commit(save_defaults=True)
|
widget.commit(save_defaults=True)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class PluginModel(QAbstractItemModel):
|
class PluginModel(QAbstractItemModel):
|
||||||
|
|
||||||
def __init__(self, *args):
|
def __init__(self, *args):
|
||||||
@ -328,14 +328,16 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
def category_current_changed(self, n, p):
|
def category_current_changed(self, n, p):
|
||||||
self.stackedWidget.setCurrentIndex(n.row())
|
self.stackedWidget.setCurrentIndex(n.row())
|
||||||
|
|
||||||
def __init__(self, window, db, server=None):
|
def __init__(self, parent, model, server=None):
|
||||||
ResizableDialog.__init__(self, window)
|
ResizableDialog.__init__(self, parent)
|
||||||
self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
|
self.ICON_SIZES = {0:QSize(48, 48), 1:QSize(32,32), 2:QSize(24,24)}
|
||||||
self._category_model = CategoryModel()
|
self._category_model = CategoryModel()
|
||||||
|
|
||||||
self.category_view.currentChanged = self.category_current_changed
|
self.category_view.currentChanged = self.category_current_changed
|
||||||
self.category_view.setModel(self._category_model)
|
self.category_view.setModel(self._category_model)
|
||||||
self.db = db
|
self.parent = parent
|
||||||
|
self.model = model
|
||||||
|
self.db = model.db
|
||||||
self.server = server
|
self.server = server
|
||||||
path = prefs['library_path']
|
path = prefs['library_path']
|
||||||
self.location.setText(path if path else '')
|
self.location.setText(path if path else '')
|
||||||
@ -359,15 +361,27 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
self.roman_numerals.setChecked(rn)
|
self.roman_numerals.setChecked(rn)
|
||||||
self.new_version_notification.setChecked(config['new_version_notification'])
|
self.new_version_notification.setChecked(config['new_version_notification'])
|
||||||
|
|
||||||
column_map = config['column_map']
|
# Set up columns
|
||||||
for col in column_map + [i for i in ALL_COLUMNS if i not in column_map]:
|
# Make copies of maps so that internal changes aren't put into the real maps
|
||||||
item = QListWidgetItem(BooksModel.headers[col], self.columns)
|
self.colmap = config['column_map'][:]
|
||||||
|
self.custcols = copy.deepcopy(self.db.custom_column_label_map)
|
||||||
|
cm = [c.decode(preferred_encoding, 'replace') for c in self.colmap]
|
||||||
|
ac = [c.decode(preferred_encoding, 'replace') for c in ALL_COLUMNS]
|
||||||
|
for col in cm + \
|
||||||
|
[i for i in ac if i not in cm] + \
|
||||||
|
[i for i in self.custcols if i not in cm]:
|
||||||
|
if col in ALL_COLUMNS:
|
||||||
|
item = QListWidgetItem(model.headers[col], self.columns)
|
||||||
|
else:
|
||||||
|
item = QListWidgetItem(self.custcols[col]['name'], self.columns)
|
||||||
item.setData(Qt.UserRole, QVariant(col))
|
item.setData(Qt.UserRole, QVariant(col))
|
||||||
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
|
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable|Qt.ItemIsSelectable)
|
||||||
item.setCheckState(Qt.Checked if col in column_map else Qt.Unchecked)
|
item.setCheckState(Qt.Checked if col in self.colmap else Qt.Unchecked)
|
||||||
|
|
||||||
self.connect(self.column_up, SIGNAL('clicked()'), self.up_column)
|
self.connect(self.column_up, SIGNAL('clicked()'), self.up_column)
|
||||||
self.connect(self.column_down, SIGNAL('clicked()'), self.down_column)
|
self.connect(self.column_down, SIGNAL('clicked()'), self.down_column)
|
||||||
|
self.connect(self.del_custcol_button, SIGNAL('clicked()'), self.del_custcol)
|
||||||
|
self.connect(self.add_custcol_button, SIGNAL('clicked()'), self.add_custcol)
|
||||||
|
self.connect(self.edit_custcol_button, SIGNAL('clicked()'), self.edit_custcol)
|
||||||
|
|
||||||
icons = config['toolbar_icon_size']
|
icons = config['toolbar_icon_size']
|
||||||
self.toolbar_button_size.setCurrentIndex(0 if icons == self.ICON_SIZES[0] else 1 if icons == self.ICON_SIZES[1] else 2)
|
self.toolbar_button_size.setCurrentIndex(0 if icons == self.ICON_SIZES[0] else 1 if icons == self.ICON_SIZES[1] else 2)
|
||||||
@ -398,7 +412,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
for item in items:
|
for item in items:
|
||||||
self.language.addItem(item[1], QVariant(item[0]))
|
self.language.addItem(item[1], QVariant(item[0]))
|
||||||
|
|
||||||
|
|
||||||
exts = set([])
|
exts = set([])
|
||||||
for ext in BOOK_EXTENSIONS:
|
for ext in BOOK_EXTENSIONS:
|
||||||
ext = ext.lower()
|
ext = ext.lower()
|
||||||
@ -633,6 +646,31 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
self.columns.insertItem(idx+1, self.columns.takeItem(idx))
|
self.columns.insertItem(idx+1, self.columns.takeItem(idx))
|
||||||
self.columns.setCurrentRow(idx+1)
|
self.columns.setCurrentRow(idx+1)
|
||||||
|
|
||||||
|
def del_custcol(self):
|
||||||
|
idx = self.columns.currentRow()
|
||||||
|
if idx < 0:
|
||||||
|
self.messagebox(_('You must select a column to delete it'))
|
||||||
|
return
|
||||||
|
col = qstring_to_unicode(self.columns.item(idx).data(Qt.UserRole).toString())
|
||||||
|
if col not in self.custcols:
|
||||||
|
self.messagebox(_('The selected column is not a custom column'))
|
||||||
|
return
|
||||||
|
ret = self.messagebox(_('Do you really want to delete column %s and all its data')%self.custcols[col]['name'],
|
||||||
|
buttons=QMessageBox.Ok|QMessageBox.Cancel,
|
||||||
|
defaultButton=QMessageBox.Cancel)
|
||||||
|
if ret != QMessageBox.Ok:
|
||||||
|
return
|
||||||
|
self.columns.item(idx).setCheckState(False)
|
||||||
|
self.columns.takeItem(idx)
|
||||||
|
self.custcols[col]['*deleteme'] = True
|
||||||
|
return
|
||||||
|
|
||||||
|
def add_custcol(self):
|
||||||
|
d = CreateCustomColumn(self, False, self.model.orig_headers, ALL_COLUMNS)
|
||||||
|
|
||||||
|
def edit_custcol(self):
|
||||||
|
d = CreateCustomColumn(self, True, self.model.orig_headers, ALL_COLUMNS)
|
||||||
|
|
||||||
def view_server_logs(self):
|
def view_server_logs(self):
|
||||||
from calibre.library.server import log_access_file, log_error_file
|
from calibre.library.server import log_access_file, log_error_file
|
||||||
d = QDialog(self)
|
d = QDialog(self)
|
||||||
@ -702,7 +740,6 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
if dir:
|
if dir:
|
||||||
self.location.setText(dir)
|
self.location.setText(dir)
|
||||||
|
|
||||||
|
|
||||||
def accept(self):
|
def accept(self):
|
||||||
mcs = unicode(self.max_cover_size.text()).strip()
|
mcs = unicode(self.max_cover_size.text()).strip()
|
||||||
if not re.match(r'\d+x\d+', mcs):
|
if not re.match(r'\d+x\d+', mcs):
|
||||||
@ -720,17 +757,38 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
wl += 1
|
wl += 1
|
||||||
config['worker_limit'] = wl
|
config['worker_limit'] = wl
|
||||||
|
|
||||||
|
|
||||||
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
|
config['use_roman_numerals_for_series_number'] = bool(self.roman_numerals.isChecked())
|
||||||
config['new_version_notification'] = bool(self.new_version_notification.isChecked())
|
config['new_version_notification'] = bool(self.new_version_notification.isChecked())
|
||||||
prefs['network_timeout'] = int(self.timeout.value())
|
prefs['network_timeout'] = int(self.timeout.value())
|
||||||
path = qstring_to_unicode(self.location.text())
|
path = qstring_to_unicode(self.location.text())
|
||||||
input_cols = [unicode(self.input_order.item(i).data(Qt.UserRole).toString()) for i in range(self.input_order.count())]
|
input_cols = [unicode(self.input_order.item(i).data(Qt.UserRole).toString()) for i in range(self.input_order.count())]
|
||||||
prefs['input_format_order'] = input_cols
|
prefs['input_format_order'] = input_cols
|
||||||
cols = [unicode(self.columns.item(i).data(Qt.UserRole).toString()) for i in range(self.columns.count()) if self.columns.item(i).checkState()==Qt.Checked]
|
|
||||||
|
####### Now deal with changes to columns
|
||||||
|
cols = [qstring_to_unicode(self.columns.item(i).data(Qt.UserRole).toString())\
|
||||||
|
for i in range(self.columns.count()) \
|
||||||
|
if self.columns.item(i).checkState()==Qt.Checked]
|
||||||
if not cols:
|
if not cols:
|
||||||
cols = ['title']
|
cols = ['title']
|
||||||
config['column_map'] = cols
|
config['column_map'] = cols
|
||||||
|
must_restart = False
|
||||||
|
for c in self.custcols:
|
||||||
|
if self.custcols[c]['num'] is None:
|
||||||
|
self.db.create_custom_column(
|
||||||
|
label=c,
|
||||||
|
name=self.custcols[c]['name'],
|
||||||
|
datatype=self.custcols[c]['datatype'],
|
||||||
|
is_multiple=self.custcols[c]['is_multiple'])
|
||||||
|
must_restart = True
|
||||||
|
elif '*deleteme' in self.custcols[c]:
|
||||||
|
self.db.delete_custom_column(label=c)
|
||||||
|
must_restart = True
|
||||||
|
elif '*edited' in self.custcols[c]:
|
||||||
|
cc = self.custcols[c]
|
||||||
|
self.db.set_custom_column_metadata(cc['num'], name=cc['name'], label=cc['label'])
|
||||||
|
if '*must_restart' in self.custcols[c]:
|
||||||
|
must_restart = True
|
||||||
|
|
||||||
config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
|
config['toolbar_icon_size'] = self.ICON_SIZES[self.toolbar_button_size.currentIndex()]
|
||||||
config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
|
config['show_text_in_toolbar'] = bool(self.show_toolbar_text.isChecked())
|
||||||
config['separate_cover_flow'] = bool(self.separate_cover_flow.isChecked())
|
config['separate_cover_flow'] = bool(self.separate_cover_flow.isChecked())
|
||||||
@ -771,8 +829,16 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
d.exec_()
|
d.exec_()
|
||||||
else:
|
else:
|
||||||
self.database_location = os.path.abspath(path)
|
self.database_location = os.path.abspath(path)
|
||||||
|
if must_restart:
|
||||||
|
self.messagebox(_('The changes you made require that Calibre be restarted. Please restart as soon as practical.'))
|
||||||
|
self.parent.must_restart_before_config = True
|
||||||
QDialog.accept(self)
|
QDialog.accept(self)
|
||||||
|
|
||||||
|
# might want to substitute the standard calibre box. However, the copy_to_clipboard
|
||||||
|
# functionality has no purpose, so ???
|
||||||
|
def messagebox(self, m, buttons=QMessageBox.Ok, defaultButton=QMessageBox.Ok):
|
||||||
|
return QMessageBox.critical(None,'Calibre configuration', m, buttons, defaultButton)
|
||||||
|
|
||||||
class VacThread(QThread):
|
class VacThread(QThread):
|
||||||
|
|
||||||
def __init__(self, parent, db):
|
def __init__(self, parent, db):
|
||||||
|
@ -498,6 +498,87 @@
|
|||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="del_custcol_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Remove a user-defined column</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/minus.svg</normaloff>:/images/minus.svg</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="add_custcol_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Add a user-defined column</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/plus.svg</normaloff>:/images/plus.svg</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="edit_custcol_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Edit settings of a user-defined column</string>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/edit_input.svg</normaloff>:/images/edit_input.svg</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QToolButton" name="column_down">
|
<widget class="QToolButton" name="column_down">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
|
123
src/calibre/gui2/dialogs/config/create_custom_column.py
Normal file
123
src/calibre/gui2/dialogs/config/create_custom_column.py
Normal 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)
|
142
src/calibre/gui2/dialogs/config/create_custom_column.ui
Normal file
142
src/calibre/gui2/dialogs/config/create_custom_column.ui
Normal 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>
|
186
src/calibre/gui2/dialogs/tag_categories.py
Normal file
186
src/calibre/gui2/dialogs/tag_categories.py
Normal 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)
|
385
src/calibre/gui2/dialogs/tag_categories.ui
Normal file
385
src/calibre/gui2/dialogs/tag_categories.ui
Normal 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&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&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>
|
@ -1,32 +1,34 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
import os, textwrap, traceback, re, shutil
|
import os, textwrap, traceback, re, shutil, functools
|
||||||
|
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
from math import cos, sin, pi
|
from math import cos, sin, pi
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
|
from datetime import date
|
||||||
|
|
||||||
from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \
|
from PyQt4.QtGui import QTableView, QAbstractItemView, QColor, \
|
||||||
QPainterPath, QLinearGradient, QBrush, \
|
QPainterPath, QLinearGradient, QBrush, \
|
||||||
QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
|
QPen, QStyle, QPainter, QStyleOptionViewItemV4, \
|
||||||
QImage, QMenu, \
|
QIcon, QImage, QMenu, \
|
||||||
QStyledItemDelegate, QCompleter
|
QStyledItemDelegate, QCompleter, QIntValidator, \
|
||||||
|
QPlainTextEdit, QDoubleValidator, QCheckBox, QMessageBox
|
||||||
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, pyqtSignal, \
|
from PyQt4.QtCore import QAbstractTableModel, QVariant, Qt, pyqtSignal, \
|
||||||
SIGNAL, QObject, QSize, QModelIndex, QDate
|
SIGNAL, QObject, QSize, QModelIndex, QDate, QRect
|
||||||
|
|
||||||
from calibre import strftime
|
from calibre import strftime
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ebooks.metadata import string_to_authors, fmt_sidx, authors_to_string
|
||||||
from calibre.utils.pyparsing import ParseException
|
|
||||||
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
|
|
||||||
from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, \
|
|
||||||
error_dialog
|
|
||||||
from calibre.gui2.widgets import EnLineEdit, TagsLineEdit
|
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
|
||||||
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
|
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
|
||||||
from calibre.ebooks.metadata import string_to_authors, fmt_sidx, \
|
from calibre.gui2 import NONE, TableView, qstring_to_unicode, config, error_dialog
|
||||||
authors_to_string
|
from calibre.gui2.dialogs.comments_dialog import CommentsDialog
|
||||||
from calibre.utils.config import tweaks
|
from calibre.gui2.widgets import EnLineEdit, TagsLineEdit
|
||||||
|
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
|
||||||
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
|
from calibre.utils.config import tweaks, prefs
|
||||||
from calibre.utils.date import dt_factory, qt_to_dt, isoformat
|
from calibre.utils.date import dt_factory, qt_to_dt, isoformat
|
||||||
|
from calibre.utils.pyparsing import ParseException
|
||||||
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
|
|
||||||
class LibraryDelegate(QStyledItemDelegate):
|
class LibraryDelegate(QStyledItemDelegate):
|
||||||
COLOR = QColor("blue")
|
COLOR = QColor("blue")
|
||||||
@ -98,7 +100,6 @@ class LibraryDelegate(QStyledItemDelegate):
|
|||||||
return sb
|
return sb
|
||||||
|
|
||||||
class DateDelegate(QStyledItemDelegate):
|
class DateDelegate(QStyledItemDelegate):
|
||||||
|
|
||||||
def displayText(self, val, locale):
|
def displayText(self, val, locale):
|
||||||
d = val.toDate()
|
d = val.toDate()
|
||||||
return d.toString('dd MMM yyyy')
|
return d.toString('dd MMM yyyy')
|
||||||
@ -114,7 +115,6 @@ class DateDelegate(QStyledItemDelegate):
|
|||||||
return qde
|
return qde
|
||||||
|
|
||||||
class PubDateDelegate(QStyledItemDelegate):
|
class PubDateDelegate(QStyledItemDelegate):
|
||||||
|
|
||||||
def displayText(self, val, locale):
|
def displayText(self, val, locale):
|
||||||
return val.toDate().toString('MMM yyyy')
|
return val.toDate().toString('MMM yyyy')
|
||||||
|
|
||||||
@ -126,7 +126,6 @@ class PubDateDelegate(QStyledItemDelegate):
|
|||||||
return qde
|
return qde
|
||||||
|
|
||||||
class TextDelegate(QStyledItemDelegate):
|
class TextDelegate(QStyledItemDelegate):
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
'''
|
'''
|
||||||
Delegate for text data. If auto_complete_function needs to return a list
|
Delegate for text data. If auto_complete_function needs to return a list
|
||||||
@ -150,7 +149,6 @@ class TextDelegate(QStyledItemDelegate):
|
|||||||
return editor
|
return editor
|
||||||
|
|
||||||
class TagsDelegate(QStyledItemDelegate):
|
class TagsDelegate(QStyledItemDelegate):
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
QStyledItemDelegate.__init__(self, parent)
|
QStyledItemDelegate.__init__(self, parent)
|
||||||
self.db = None
|
self.db = None
|
||||||
@ -160,17 +158,101 @@ class TagsDelegate(QStyledItemDelegate):
|
|||||||
|
|
||||||
def createEditor(self, parent, option, index):
|
def createEditor(self, parent, option, index):
|
||||||
if self.db:
|
if self.db:
|
||||||
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:
|
else:
|
||||||
editor = EnLineEdit(parent)
|
editor = EnLineEdit(parent)
|
||||||
return editor
|
return editor
|
||||||
|
|
||||||
|
class CcTextDelegate(QStyledItemDelegate):
|
||||||
|
def __init__(self, parent):
|
||||||
|
'''
|
||||||
|
Delegate for text/int/float data.
|
||||||
|
'''
|
||||||
|
QStyledItemDelegate.__init__(self, parent)
|
||||||
|
def createEditor(self, parent, option, index):
|
||||||
|
m = index.model()
|
||||||
|
col = m.column_map[index.column()]
|
||||||
|
typ = m.custom_columns[col]['datatype']
|
||||||
|
editor = EnLineEdit(parent)
|
||||||
|
if typ == 'int':
|
||||||
|
editor.setValidator(QIntValidator(parent))
|
||||||
|
elif typ == 'float':
|
||||||
|
editor.setValidator(QDoubleValidator(parent))
|
||||||
|
else:
|
||||||
|
complete_items = sorted(list(m.db.all_custom(label=col)))
|
||||||
|
completer = QCompleter(complete_items, self)
|
||||||
|
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||||||
|
completer.setCompletionMode(QCompleter.PopupCompletion)
|
||||||
|
editor.setCompleter(completer)
|
||||||
|
return editor
|
||||||
|
|
||||||
|
class CcCommentsDelegate(QStyledItemDelegate):
|
||||||
|
def __init__(self, parent):
|
||||||
|
'''
|
||||||
|
Delegate for comments data.
|
||||||
|
'''
|
||||||
|
QStyledItemDelegate.__init__(self, parent)
|
||||||
|
self.parent = parent
|
||||||
|
|
||||||
|
def createEditor(self, parent, option, index):
|
||||||
|
m = index.model()
|
||||||
|
col = m.column_map[index.column()]
|
||||||
|
# db col is not named for the field, but for the table number. To get it,
|
||||||
|
# gui column -> column label -> table number -> db column
|
||||||
|
text = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[col]['num']]]
|
||||||
|
editor = CommentsDialog(parent, text)
|
||||||
|
d = editor.exec_()
|
||||||
|
if d:
|
||||||
|
m.setData(index, QVariant(editor.textbox.toPlainText()), Qt.EditRole)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def setModelData(self, editor, model, index):
|
||||||
|
model.setData(index, QVariant(editor.textbox.text()), Qt.EditRole)
|
||||||
|
|
||||||
|
class CcBoolDelegate(QStyledItemDelegate):
|
||||||
|
def __init__(self, parent):
|
||||||
|
'''
|
||||||
|
Delegate for custom_column bool data.
|
||||||
|
'''
|
||||||
|
QStyledItemDelegate.__init__(self, parent)
|
||||||
|
|
||||||
|
def createEditor(self, parent, option, index):
|
||||||
|
m = index.model()
|
||||||
|
col = m.column_map[index.column()]
|
||||||
|
editor = QCheckBox(parent)
|
||||||
|
val = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[col]['num']]]
|
||||||
|
if tweaks['bool_custom_columns_are_tristate'] == 'no':
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
if tweaks['bool_custom_columns_are_tristate'] == 'yes':
|
||||||
|
editor.setTristate(True)
|
||||||
|
return editor
|
||||||
|
|
||||||
|
def setModelData(self, editor, model, index):
|
||||||
|
model.setData(index, QVariant(editor.checkState()), Qt.EditRole)
|
||||||
|
|
||||||
|
def setEditorData(self, editor, index):
|
||||||
|
m = index.model()
|
||||||
|
# db col is not named for the field, but for the table number. To get it,
|
||||||
|
# gui column -> column label -> table number -> db column
|
||||||
|
val = m.db.data[index.row()][m.db.FIELD_MAP[m.custom_columns[m.column_map[index.column()]]['num']]]
|
||||||
|
if tweaks['bool_custom_columns_are_tristate'] == 'no':
|
||||||
|
val = Qt.Unchecked if val is None or not val else Qt.Checked
|
||||||
|
else:
|
||||||
|
val = Qt.PartiallyChecked if val is None else Qt.Unchecked if not val else Qt.Checked
|
||||||
|
editor.setCheckState(val)
|
||||||
|
|
||||||
class BooksModel(QAbstractTableModel):
|
class BooksModel(QAbstractTableModel):
|
||||||
|
|
||||||
about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted')
|
about_to_be_sorted = pyqtSignal(object, name='aboutToBeSorted')
|
||||||
sorting_done = pyqtSignal(object, name='sortingDone')
|
sorting_done = pyqtSignal(object, name='sortingDone')
|
||||||
|
|
||||||
headers = {
|
orig_headers = {
|
||||||
'title' : _("Title"),
|
'title' : _("Title"),
|
||||||
'authors' : _("Author(s)"),
|
'authors' : _("Author(s)"),
|
||||||
'size' : _("Size (MB)"),
|
'size' : _("Size (MB)"),
|
||||||
@ -185,15 +267,22 @@ class BooksModel(QAbstractTableModel):
|
|||||||
def __init__(self, parent=None, buffer=40):
|
def __init__(self, parent=None, buffer=40):
|
||||||
QAbstractTableModel.__init__(self, parent)
|
QAbstractTableModel.__init__(self, parent)
|
||||||
self.db = None
|
self.db = None
|
||||||
self.column_map = config['column_map']
|
|
||||||
self.editable_cols = ['title', 'authors', 'rating', 'publisher',
|
self.editable_cols = ['title', 'authors', 'rating', 'publisher',
|
||||||
'tags', 'series', 'timestamp', 'pubdate']
|
'tags', 'series', 'timestamp', 'pubdate']
|
||||||
self.default_image = QImage(I('book.svg'))
|
self.default_image = QImage(I('book.svg'))
|
||||||
self.sorted_on = ('timestamp', Qt.AscendingOrder)
|
self.sorted_on = ('timestamp', Qt.AscendingOrder)
|
||||||
|
self.sort_history = [self.sorted_on]
|
||||||
self.last_search = '' # The last search performed on this model
|
self.last_search = '' # The last search performed on this model
|
||||||
self.read_config()
|
self.column_map = []
|
||||||
|
self.headers = {}
|
||||||
self.buffer_size = buffer
|
self.buffer_size = buffer
|
||||||
self.cover_cache = None
|
self.cover_cache = None
|
||||||
|
self.bool_yes_icon = QIcon(I('ok.svg'))
|
||||||
|
self.bool_no_icon = QIcon(I('list_remove.svg'))
|
||||||
|
self.bool_blank_icon = QIcon(I('blank.svg'))
|
||||||
|
|
||||||
|
def is_custom_column(self, cc_label):
|
||||||
|
return cc_label in self.custom_columns
|
||||||
|
|
||||||
def clear_caches(self):
|
def clear_caches(self):
|
||||||
if self.cover_cache:
|
if self.cover_cache:
|
||||||
@ -201,15 +290,24 @@ class BooksModel(QAbstractTableModel):
|
|||||||
|
|
||||||
def read_config(self):
|
def read_config(self):
|
||||||
self.use_roman_numbers = config['use_roman_numerals_for_series_number']
|
self.use_roman_numbers = config['use_roman_numerals_for_series_number']
|
||||||
cols = config['column_map']
|
self.column_map = config['column_map'][:] # force a copy
|
||||||
if cols != self.column_map:
|
self.headers = {}
|
||||||
self.column_map = cols
|
for i in self.column_map: # take out any columns no longer in the db
|
||||||
self.reset()
|
if not i in self.orig_headers and not i in self.custom_columns:
|
||||||
self.emit(SIGNAL('columns_sorted()'))
|
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):
|
def set_database(self, db):
|
||||||
self.db = db
|
self.db = db
|
||||||
self.build_data_convertors()
|
self.custom_columns = self.db.custom_column_label_map
|
||||||
|
self.read_config()
|
||||||
|
|
||||||
def refresh_ids(self, ids, current_row=-1):
|
def refresh_ids(self, ids, current_row=-1):
|
||||||
rows = self.db.refresh_ids(ids)
|
rows = self.db.refresh_ids(ids)
|
||||||
@ -316,6 +414,8 @@ class BooksModel(QAbstractTableModel):
|
|||||||
self.clear_caches()
|
self.clear_caches()
|
||||||
self.reset()
|
self.reset()
|
||||||
self.sorted_on = (self.column_map[col], order)
|
self.sorted_on = (self.column_map[col], order)
|
||||||
|
self.sort_history.insert(0, self.sorted_on)
|
||||||
|
del self.sort_history[3:] # clean up older searches
|
||||||
self.sorting_done.emit(self.db.index)
|
self.sorting_done.emit(self.db.index)
|
||||||
|
|
||||||
def refresh(self, reset=True):
|
def refresh(self, reset=True):
|
||||||
@ -323,10 +423,8 @@ class BooksModel(QAbstractTableModel):
|
|||||||
col = self.column_map.index(self.sorted_on[0])
|
col = self.column_map.index(self.sorted_on[0])
|
||||||
except:
|
except:
|
||||||
col = 0
|
col = 0
|
||||||
self.db.refresh(field=self.column_map[col],
|
self.db.refresh(field=None)
|
||||||
ascending=self.sorted_on[1]==Qt.AscendingOrder)
|
self.sort(col, self.sorted_on[1], reset=reset)
|
||||||
if reset:
|
|
||||||
self.reset()
|
|
||||||
|
|
||||||
def resort(self, reset=True):
|
def resort(self, reset=True):
|
||||||
try:
|
try:
|
||||||
@ -430,6 +528,7 @@ class BooksModel(QAbstractTableModel):
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
def get_metadata(self, rows, rows_are_ids=False, full_metadata=False):
|
def get_metadata(self, rows, rows_are_ids=False, full_metadata=False):
|
||||||
|
# Should this add the custom columns? It doesn't at the moment
|
||||||
metadata, _full_metadata = [], []
|
metadata, _full_metadata = [], []
|
||||||
if not rows_are_ids:
|
if not rows_are_ids:
|
||||||
rows = [self.db.id(row.row()) for row in rows]
|
rows = [self.db.id(row.row()) for row in rows]
|
||||||
@ -562,75 +661,119 @@ class BooksModel(QAbstractTableModel):
|
|||||||
return img
|
return img
|
||||||
|
|
||||||
def build_data_convertors(self):
|
def build_data_convertors(self):
|
||||||
|
def authors(r, idx=-1):
|
||||||
tidx = self.db.FIELD_MAP['title']
|
au = self.db.data[r][idx]
|
||||||
aidx = self.db.FIELD_MAP['authors']
|
|
||||||
sidx = self.db.FIELD_MAP['size']
|
|
||||||
ridx = self.db.FIELD_MAP['rating']
|
|
||||||
pidx = self.db.FIELD_MAP['publisher']
|
|
||||||
tmdx = self.db.FIELD_MAP['timestamp']
|
|
||||||
pddx = self.db.FIELD_MAP['pubdate']
|
|
||||||
srdx = self.db.FIELD_MAP['series']
|
|
||||||
tgdx = self.db.FIELD_MAP['tags']
|
|
||||||
siix = self.db.FIELD_MAP['series_index']
|
|
||||||
|
|
||||||
def authors(r):
|
|
||||||
au = self.db.data[r][aidx]
|
|
||||||
if au:
|
if au:
|
||||||
au = [a.strip().replace('|', ',') for a in au.split(',')]
|
au = [a.strip().replace('|', ',') for a in au.split(',')]
|
||||||
return ' & '.join(au)
|
return QVariant(' & '.join(au))
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
def timestamp(r):
|
def tags(r, idx=-1):
|
||||||
dt = self.db.data[r][tmdx]
|
tags = self.db.data[r][idx]
|
||||||
if dt:
|
|
||||||
return QDate(dt.year, dt.month, dt.day)
|
|
||||||
|
|
||||||
def pubdate(r):
|
|
||||||
dt = self.db.data[r][pddx]
|
|
||||||
if dt:
|
|
||||||
return QDate(dt.year, dt.month, dt.day)
|
|
||||||
|
|
||||||
def rating(r):
|
|
||||||
r = self.db.data[r][ridx]
|
|
||||||
r = r/2 if r else 0
|
|
||||||
return r
|
|
||||||
|
|
||||||
def publisher(r):
|
|
||||||
pub = self.db.data[r][pidx]
|
|
||||||
if pub:
|
|
||||||
return pub
|
|
||||||
|
|
||||||
def tags(r):
|
|
||||||
tags = self.db.data[r][tgdx]
|
|
||||||
if tags:
|
if tags:
|
||||||
return ', '.join(sorted(tags.split(',')))
|
return QVariant(', '.join(sorted(tags.split(','))))
|
||||||
|
return None
|
||||||
|
|
||||||
def series(r):
|
def series(r, idx=-1, siix=-1):
|
||||||
series = self.db.data[r][srdx]
|
series = self.db.data[r][idx]
|
||||||
if series:
|
if series:
|
||||||
idx = fmt_sidx(self.db.data[r][siix])
|
idx = fmt_sidx(self.db.data[r][siix])
|
||||||
return series + ' [%s]'%idx
|
return QVariant(series + ' [%s]'%idx)
|
||||||
def size(r):
|
return None
|
||||||
size = self.db.data[r][sidx]
|
|
||||||
|
def size(r, idx=-1):
|
||||||
|
size = self.db.data[r][idx]
|
||||||
if size:
|
if size:
|
||||||
return '%.1f'%(float(size)/(1024*1024))
|
return QVariant('%.1f'%(float(size)/(1024*1024)))
|
||||||
|
return None
|
||||||
|
|
||||||
|
def rating_type(r, idx=-1):
|
||||||
|
r = self.db.data[r][idx]
|
||||||
|
r = r/2 if r else 0
|
||||||
|
return QVariant(r)
|
||||||
|
|
||||||
|
def datetime_type(r, idx=-1):
|
||||||
|
val = self.db.data[r][idx]
|
||||||
|
if val is not None:
|
||||||
|
return QVariant(QDate(val))
|
||||||
|
else:
|
||||||
|
return QVariant(QDate())
|
||||||
|
|
||||||
|
def bool_type(r, idx=-1):
|
||||||
|
return None # displayed using a decorator
|
||||||
|
|
||||||
|
def bool_type_decorator(r, idx=-1, bool_cols_are_tristate=True):
|
||||||
|
val = self.db.data[r][idx]
|
||||||
|
if not bool_cols_are_tristate:
|
||||||
|
if val is None or not val:
|
||||||
|
return self.bool_no_icon
|
||||||
|
if val:
|
||||||
|
return self.bool_yes_icon
|
||||||
|
if val is None:
|
||||||
|
return self.bool_blank_icon
|
||||||
|
return self.bool_no_icon
|
||||||
|
|
||||||
|
def text_type(r, mult=False, idx=-1):
|
||||||
|
text = self.db.data[r][idx]
|
||||||
|
if text and mult:
|
||||||
|
return QVariant(', '.join(sorted(text.split('|'))))
|
||||||
|
return QVariant(text)
|
||||||
|
|
||||||
|
def number_type(r, idx=-1):
|
||||||
|
return QVariant(self.db.data[r][idx])
|
||||||
|
|
||||||
self.dc = {
|
self.dc = {
|
||||||
'title' : lambda r : self.db.data[r][tidx],
|
'title' : functools.partial(text_type, idx=self.db.FIELD_MAP['title'], mult=False),
|
||||||
'authors' : authors,
|
'authors' : functools.partial(authors, idx=self.db.FIELD_MAP['authors']),
|
||||||
'size' : size,
|
'size' : functools.partial(size, idx=self.db.FIELD_MAP['size']),
|
||||||
'timestamp': timestamp,
|
'timestamp': functools.partial(datetime_type, idx=self.db.FIELD_MAP['timestamp']),
|
||||||
'pubdate' : pubdate,
|
'pubdate' : functools.partial(datetime_type, idx=self.db.FIELD_MAP['pubdate']),
|
||||||
'rating' : rating,
|
'rating' : functools.partial(rating_type, idx=self.db.FIELD_MAP['rating']),
|
||||||
'publisher': publisher,
|
'publisher': functools.partial(text_type, idx=self.db.FIELD_MAP['title'], mult=False),
|
||||||
'tags' : tags,
|
'tags' : functools.partial(tags, idx=self.db.FIELD_MAP['tags']),
|
||||||
'series' : series,
|
'series' : functools.partial(series, idx=self.db.FIELD_MAP['series'], siix=self.db.FIELD_MAP['series_index']),
|
||||||
}
|
}
|
||||||
|
self.dc_decorator = {}
|
||||||
|
|
||||||
|
# Add the custom columns to the data converters
|
||||||
|
for col in self.custom_columns:
|
||||||
|
idx = self.db.FIELD_MAP[self.custom_columns[col]['num']]
|
||||||
|
datatype = self.custom_columns[col]['datatype']
|
||||||
|
if datatype in ('text', 'comments'):
|
||||||
|
self.dc[col] = functools.partial(text_type, idx=idx, mult=self.custom_columns[col]['is_multiple'])
|
||||||
|
elif datatype in ('int', 'float'):
|
||||||
|
self.dc[col] = functools.partial(number_type, idx=idx)
|
||||||
|
elif datatype == 'datetime':
|
||||||
|
self.dc[col] = functools.partial(datetime_type, idx=idx)
|
||||||
|
elif datatype == 'bool':
|
||||||
|
self.dc[col] = functools.partial(bool_type, idx=idx)
|
||||||
|
self.dc_decorator[col] = functools.partial(
|
||||||
|
bool_type_decorator, idx=idx,
|
||||||
|
bool_cols_are_tristate=tweaks['bool_custom_columns_are_tristate'] == 'yes')
|
||||||
|
elif datatype == 'rating':
|
||||||
|
self.dc[col] = functools.partial(rating_type, idx=idx)
|
||||||
|
else:
|
||||||
|
print 'What type is this?', col, datatype
|
||||||
|
# build a index column to data converter map, to remove the string lookup in the data loop
|
||||||
|
self.column_to_dc_map = []
|
||||||
|
self.column_to_dc_decorator_map = []
|
||||||
|
for col in self.column_map:
|
||||||
|
self.column_to_dc_map.append(self.dc[col])
|
||||||
|
self.column_to_dc_decorator_map.append(self.dc_decorator.get(col, None))
|
||||||
|
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
|
col = index.column()
|
||||||
|
# in obscure cases where custom columns are both edited and added, for a time
|
||||||
|
# the column map does not accurately represent the screen. In these cases,
|
||||||
|
# we will get asked to display columns we don't know about. Must test for this.
|
||||||
|
if col >= len(self.column_to_dc_map):
|
||||||
|
return None
|
||||||
if role in (Qt.DisplayRole, Qt.EditRole):
|
if role in (Qt.DisplayRole, Qt.EditRole):
|
||||||
ans = self.dc[self.column_map[index.column()]](index.row())
|
return self.column_to_dc_map[col](index.row())
|
||||||
return NONE if ans is None else QVariant(ans)
|
elif role == Qt.DecorationRole:
|
||||||
|
if self.column_to_dc_decorator_map[col] is not None:
|
||||||
|
return self.column_to_dc_decorator_map[index.column()](index.row())
|
||||||
#elif role == Qt.TextAlignmentRole and self.column_map[index.column()] in ('size', 'timestamp'):
|
#elif role == Qt.TextAlignmentRole and self.column_map[index.column()] in ('size', 'timestamp'):
|
||||||
# return QVariant(Qt.AlignVCenter | Qt.AlignCenter)
|
# return QVariant(Qt.AlignVCenter | Qt.AlignCenter)
|
||||||
#elif role == Qt.ToolTipRole and index.isValid():
|
#elif role == Qt.ToolTipRole and index.isValid():
|
||||||
@ -639,65 +782,105 @@ class BooksModel(QAbstractTableModel):
|
|||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
def headerData(self, section, orientation, role):
|
def headerData(self, section, orientation, role):
|
||||||
if role != Qt.DisplayRole:
|
|
||||||
return NONE
|
|
||||||
if orientation == Qt.Horizontal:
|
if orientation == Qt.Horizontal:
|
||||||
return QVariant(self.headers[self.column_map[section]])
|
if section >= len(self.column_map): # same problem as in data, the column_map can be wrong
|
||||||
else:
|
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 QVariant(section+1)
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
|
||||||
def flags(self, index):
|
def flags(self, index):
|
||||||
flags = QAbstractTableModel.flags(self, index)
|
flags = QAbstractTableModel.flags(self, index)
|
||||||
if index.isValid():
|
if index.isValid():
|
||||||
if self.column_map[index.column()] in self.editable_cols:
|
colhead = self.column_map[index.column()]
|
||||||
|
if colhead in self.editable_cols:
|
||||||
flags |= Qt.ItemIsEditable
|
flags |= Qt.ItemIsEditable
|
||||||
|
elif self.is_custom_column(colhead):
|
||||||
|
if self.custom_columns[colhead]['editable']:
|
||||||
|
flags |= Qt.ItemIsEditable
|
||||||
return flags
|
return flags
|
||||||
|
|
||||||
|
def set_custom_column_data(self, row, colhead, value):
|
||||||
|
typ = self.custom_columns[colhead]['datatype']
|
||||||
|
if typ in ('text', 'comments'):
|
||||||
|
val = qstring_to_unicode(value.toString()).strip()
|
||||||
|
val = val if val else None
|
||||||
|
if typ == 'bool':
|
||||||
|
val = value.toInt()[0] # tristate checkboxes put unknown in the middle
|
||||||
|
val = None if val == 1 else False if val == 0 else True
|
||||||
|
elif typ == 'rating':
|
||||||
|
val = value.toInt()[0]
|
||||||
|
val = 0 if val < 0 else 5 if val > 5 else val
|
||||||
|
val *= 2
|
||||||
|
elif typ in ('int', 'float'):
|
||||||
|
val = qstring_to_unicode(value.toString()).strip()
|
||||||
|
if val is None or not val:
|
||||||
|
val = None
|
||||||
|
elif typ == 'datetime':
|
||||||
|
val = value.toDate()
|
||||||
|
if val.isNull() or not val.isValid():
|
||||||
|
return False
|
||||||
|
val = qt_to_dt(val, as_utc=False)
|
||||||
|
self.db.set_custom(self.db.id(row), val, label=colhead, num=None, append=False, notify=True)
|
||||||
|
return True
|
||||||
|
|
||||||
def setData(self, index, value, role):
|
def setData(self, index, value, role):
|
||||||
if role == Qt.EditRole:
|
if role == Qt.EditRole:
|
||||||
row, col = index.row(), index.column()
|
row, col = index.row(), index.column()
|
||||||
column = self.column_map[col]
|
column = self.column_map[col]
|
||||||
if column not in self.editable_cols:
|
if self.is_custom_column(column):
|
||||||
return False
|
if not self.set_custom_column_data(row, column, value):
|
||||||
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
|
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:
|
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)"), \
|
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), \
|
||||||
index, index)
|
index, index)
|
||||||
if column == self.sorted_on[0]:
|
if column == self.sorted_on[0]:
|
||||||
self.resort()
|
self.resort()
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def set_search_restriction(self, s):
|
||||||
|
self.db.data.set_search_restriction(s)
|
||||||
|
|
||||||
class BooksView(TableView):
|
class BooksView(TableView):
|
||||||
TIME_FMT = '%d %b %Y'
|
TIME_FMT = '%d %b %Y'
|
||||||
wrapper = textwrap.TextWrapper(width=20)
|
wrapper = textwrap.TextWrapper(width=20)
|
||||||
@ -721,6 +904,9 @@ class BooksView(TableView):
|
|||||||
self.authors_delegate = TextDelegate(self)
|
self.authors_delegate = TextDelegate(self)
|
||||||
self.series_delegate = TextDelegate(self)
|
self.series_delegate = TextDelegate(self)
|
||||||
self.publisher_delegate = TextDelegate(self)
|
self.publisher_delegate = TextDelegate(self)
|
||||||
|
self.cc_text_delegate = CcTextDelegate(self)
|
||||||
|
self.cc_bool_delegate = CcBoolDelegate(self)
|
||||||
|
self.cc_comments_delegate = CcCommentsDelegate(self)
|
||||||
self.display_parent = parent
|
self.display_parent = parent
|
||||||
self._model = modelcls(self)
|
self._model = modelcls(self)
|
||||||
self.setModel(self._model)
|
self.setModel(self._model)
|
||||||
@ -775,6 +961,25 @@ class BooksView(TableView):
|
|||||||
self.setItemDelegateForColumn(cm.index('publisher'), self.publisher_delegate)
|
self.setItemDelegateForColumn(cm.index('publisher'), self.publisher_delegate)
|
||||||
if 'series' in cm:
|
if 'series' in cm:
|
||||||
self.setItemDelegateForColumn(cm.index('series'), self.series_delegate)
|
self.setItemDelegateForColumn(cm.index('series'), self.series_delegate)
|
||||||
|
for colhead in cm:
|
||||||
|
if not self._model.is_custom_column(colhead):
|
||||||
|
continue
|
||||||
|
cc = self._model.custom_columns[colhead]
|
||||||
|
if cc['datatype'] == 'datetime':
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead), self.timestamp_delegate)
|
||||||
|
elif cc['datatype'] == 'comments':
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead), self.cc_comments_delegate)
|
||||||
|
elif cc['datatype'] == 'text':
|
||||||
|
if cc['is_multiple']:
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead), self.tags_delegate)
|
||||||
|
else:
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
|
||||||
|
elif cc['datatype'] in ('int', 'float'):
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead), self.cc_text_delegate)
|
||||||
|
elif cc['datatype'] == 'bool':
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead), self.cc_bool_delegate)
|
||||||
|
elif cc['datatype'] == 'rating':
|
||||||
|
self.setItemDelegateForColumn(cm.index(colhead), self.rating_delegate)
|
||||||
|
|
||||||
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
||||||
save, open_folder, book_details, delete, similar_menu=None):
|
save, open_folder, book_details, delete, similar_menu=None):
|
||||||
@ -801,6 +1006,16 @@ class BooksView(TableView):
|
|||||||
self.context_menu.popup(event.globalPos())
|
self.context_menu.popup(event.globalPos())
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
|
def restore_sort_at_startup(self, saved_history):
|
||||||
|
if tweaks['sort_columns_at_startup'] is not None:
|
||||||
|
saved_history = tweaks['sort_columns_at_startup']
|
||||||
|
|
||||||
|
if saved_history is None:
|
||||||
|
return
|
||||||
|
for col,order in reversed(saved_history):
|
||||||
|
self.sortByColumn(col, order)
|
||||||
|
self.model().sort_history = saved_history
|
||||||
|
|
||||||
def sortByColumn(self, colname, order):
|
def sortByColumn(self, colname, order):
|
||||||
try:
|
try:
|
||||||
idx = self._model.column_map.index(colname)
|
idx = self._model.column_map.index(colname)
|
||||||
@ -836,7 +1051,6 @@ class BooksView(TableView):
|
|||||||
event.accept()
|
event.accept()
|
||||||
self.emit(SIGNAL('files_dropped(PyQt_PyObject)'), paths)
|
self.emit(SIGNAL('files_dropped(PyQt_PyObject)'), paths)
|
||||||
|
|
||||||
|
|
||||||
def set_database(self, db):
|
def set_database(self, db):
|
||||||
self._model.set_database(db)
|
self._model.set_database(db)
|
||||||
self.tags_delegate.set_database(db)
|
self.tags_delegate.set_database(db)
|
||||||
@ -857,6 +1071,10 @@ class BooksView(TableView):
|
|||||||
self.connect(self._model, SIGNAL('searched(PyQt_PyObject)'),
|
self.connect(self._model, SIGNAL('searched(PyQt_PyObject)'),
|
||||||
self.search_done)
|
self.search_done)
|
||||||
|
|
||||||
|
def connect_to_restriction_set(self, tv):
|
||||||
|
QObject.connect(tv, SIGNAL('restriction_set(PyQt_PyObject)'),
|
||||||
|
self._model.set_search_restriction)
|
||||||
|
|
||||||
def connect_to_book_display(self, bd):
|
def connect_to_book_display(self, bd):
|
||||||
QObject.connect(self._model, SIGNAL('new_bookdisplay_data(PyQt_PyObject)'),
|
QObject.connect(self._model, SIGNAL('new_bookdisplay_data(PyQt_PyObject)'),
|
||||||
bd)
|
bd)
|
||||||
@ -952,7 +1170,6 @@ class OnDeviceSearch(SearchQueryParser):
|
|||||||
matches.add(index)
|
matches.add(index)
|
||||||
break
|
break
|
||||||
except ValueError: # Unicode errors
|
except ValueError: # Unicode errors
|
||||||
import traceback
|
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
@ -1190,5 +1407,6 @@ class DeviceBooksModel(BooksModel):
|
|||||||
def set_editable(self, editable):
|
def set_editable(self, editable):
|
||||||
self.editable = editable
|
self.editable = editable
|
||||||
|
|
||||||
|
def set_search_restriction(self, s):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@ -306,6 +306,12 @@
|
|||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="TagsView" name="tags_view">
|
<widget class="TagsView" name="tags_view">
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>256</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
<property name="tabKeyNavigation">
|
<property name="tabKeyNavigation">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
@ -328,21 +334,59 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QComboBox" name="tag_match">
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
<property name="currentIndex">
|
|
||||||
<number>0</number>
|
|
||||||
</property>
|
|
||||||
<item>
|
<item>
|
||||||
<property name="text">
|
<widget class="QComboBox" name="tag_match">
|
||||||
<string>Match any</string>
|
<property name="currentIndex">
|
||||||
</property>
|
<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>
|
||||||
<item>
|
<item>
|
||||||
<property name="text">
|
<widget class="QPushButton" name="edit_categories">
|
||||||
<string>Match all</string>
|
<property name="text">
|
||||||
</property>
|
<string>Manage user categories</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Create, edit, and delete user categories</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
</item>
|
</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>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
@ -10,6 +10,7 @@ from PyQt4.Qt import QComboBox, SIGNAL, Qt, QLineEdit, QStringList, pyqtSlot
|
|||||||
from PyQt4.QtGui import QCompleter
|
from PyQt4.QtGui import QCompleter
|
||||||
|
|
||||||
from calibre.gui2 import config
|
from calibre.gui2 import config
|
||||||
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
|
|
||||||
class SearchLineEdit(QLineEdit):
|
class SearchLineEdit(QLineEdit):
|
||||||
|
|
||||||
@ -226,7 +227,6 @@ class SavedSearchBox(QComboBox):
|
|||||||
self.clear_to_help()
|
self.clear_to_help()
|
||||||
|
|
||||||
def normalize_state(self):
|
def normalize_state(self):
|
||||||
#print 'in normalize_state'
|
|
||||||
self.setEditText('')
|
self.setEditText('')
|
||||||
self.line_edit.setStyleSheet(
|
self.line_edit.setStyleSheet(
|
||||||
'QLineEdit { color: black; background-color: %s; }' %
|
'QLineEdit { color: black; background-color: %s; }' %
|
||||||
@ -234,7 +234,6 @@ class SavedSearchBox(QComboBox):
|
|||||||
self.help_state = False
|
self.help_state = False
|
||||||
|
|
||||||
def clear_to_help(self):
|
def clear_to_help(self):
|
||||||
#print 'in clear_to_help'
|
|
||||||
self.setToolTip(self.tool_tip_text)
|
self.setToolTip(self.tool_tip_text)
|
||||||
self.initialize_saved_search_names()
|
self.initialize_saved_search_names()
|
||||||
self.setEditText(self.help_text)
|
self.setEditText(self.help_text)
|
||||||
@ -245,12 +244,10 @@ class SavedSearchBox(QComboBox):
|
|||||||
self.normal_background)
|
self.normal_background)
|
||||||
|
|
||||||
def focus_out(self, event):
|
def focus_out(self, event):
|
||||||
#print 'in focus_out'
|
|
||||||
if self.currentText() == '':
|
if self.currentText() == '':
|
||||||
self.clear_to_help()
|
self.clear_to_help()
|
||||||
|
|
||||||
def key_pressed(self, event):
|
def key_pressed(self, event):
|
||||||
#print 'in key_pressed'
|
|
||||||
if self.help_state:
|
if self.help_state:
|
||||||
self.normalize_state()
|
self.normalize_state()
|
||||||
|
|
||||||
@ -259,7 +256,6 @@ class SavedSearchBox(QComboBox):
|
|||||||
self.normalize_state()
|
self.normalize_state()
|
||||||
|
|
||||||
def saved_search_selected (self, qname):
|
def saved_search_selected (self, qname):
|
||||||
#print 'in saved_search_selected'
|
|
||||||
qname = unicode(qname)
|
qname = unicode(qname)
|
||||||
if qname is None or not qname.strip():
|
if qname is None or not qname.strip():
|
||||||
return
|
return
|
||||||
@ -269,7 +265,6 @@ class SavedSearchBox(QComboBox):
|
|||||||
self.setToolTip(self.saved_searches.lookup(qname))
|
self.setToolTip(self.saved_searches.lookup(qname))
|
||||||
|
|
||||||
def initialize_saved_search_names(self):
|
def initialize_saved_search_names(self):
|
||||||
#print 'in initialize_saved_search_names'
|
|
||||||
self.clear()
|
self.clear()
|
||||||
qnames = self.saved_searches.names()
|
qnames = self.saved_searches.names()
|
||||||
self.addItems(qnames)
|
self.addItems(qnames)
|
||||||
@ -277,7 +272,10 @@ class SavedSearchBox(QComboBox):
|
|||||||
|
|
||||||
# SIGNALed from the main UI
|
# SIGNALed from the main UI
|
||||||
def delete_search_button_clicked(self):
|
def delete_search_button_clicked(self):
|
||||||
#print 'in delete_search_button_clicked'
|
if not confirm('<p>'+_('The selected search will be '
|
||||||
|
'<b>permanently deleted</b>. Are you sure?')
|
||||||
|
+'</p>', 'saved_search_delete', self):
|
||||||
|
return
|
||||||
idx = self.currentIndex
|
idx = self.currentIndex
|
||||||
if idx < 0:
|
if idx < 0:
|
||||||
return
|
return
|
||||||
@ -288,7 +286,6 @@ class SavedSearchBox(QComboBox):
|
|||||||
|
|
||||||
# SIGNALed from the main UI
|
# SIGNALed from the main UI
|
||||||
def save_search_button_clicked(self):
|
def save_search_button_clicked(self):
|
||||||
#print 'in save_search_button_clicked'
|
|
||||||
name = unicode(self.currentText())
|
name = unicode(self.currentText())
|
||||||
if self.help_state or not name.strip():
|
if self.help_state or not name.strip():
|
||||||
name = unicode(self.search_box.text()).replace('"', '')
|
name = unicode(self.search_box.text()).replace('"', '')
|
||||||
@ -305,10 +302,7 @@ class SavedSearchBox(QComboBox):
|
|||||||
|
|
||||||
# SIGNALed from the main UI
|
# SIGNALed from the main UI
|
||||||
def copy_search_button_clicked (self):
|
def copy_search_button_clicked (self):
|
||||||
#print 'in copy_search_button_clicked'
|
|
||||||
idx = self.currentIndex();
|
idx = self.currentIndex();
|
||||||
if idx < 0:
|
if idx < 0:
|
||||||
return
|
return
|
||||||
self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText())))
|
self.search_box.set_search_string(self.saved_searches.lookup(unicode(self.currentText())))
|
||||||
|
|
||||||
|
|
||||||
|
@ -8,11 +8,13 @@ Browsing book collection by tags.
|
|||||||
'''
|
'''
|
||||||
|
|
||||||
from itertools import izip
|
from itertools import izip
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
|
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
|
||||||
QFont, SIGNAL, QSize, QIcon, QPoint, \
|
QFont, SIGNAL, QSize, QIcon, QPoint, \
|
||||||
QAbstractItemModel, QVariant, QModelIndex
|
QAbstractItemModel, QVariant, QModelIndex
|
||||||
from calibre.gui2 import config, NONE
|
from calibre.gui2 import config, NONE
|
||||||
|
from calibre.utils.config import prefs
|
||||||
from calibre.utils.search_query_parser import saved_searches
|
from calibre.utils.search_query_parser import saved_searches
|
||||||
from calibre.library.database2 import Tag
|
from calibre.library.database2 import Tag
|
||||||
|
|
||||||
@ -27,16 +29,24 @@ class TagsView(QTreeView):
|
|||||||
self.setIconSize(QSize(30, 30))
|
self.setIconSize(QSize(30, 30))
|
||||||
self.tag_match = None
|
self.tag_match = None
|
||||||
|
|
||||||
def set_database(self, db, tag_match, popularity):
|
def set_database(self, db, tag_match, popularity, restriction):
|
||||||
self._model = TagsModel(db, parent=self)
|
self._model = TagsModel(db, parent=self)
|
||||||
self.popularity = popularity
|
self.popularity = popularity
|
||||||
|
self.restriction = restriction
|
||||||
self.tag_match = tag_match
|
self.tag_match = tag_match
|
||||||
|
self.db = db
|
||||||
self.setModel(self._model)
|
self.setModel(self._model)
|
||||||
self.connect(self, SIGNAL('clicked(QModelIndex)'), self.toggle)
|
self.connect(self, SIGNAL('clicked(QModelIndex)'), self.toggle)
|
||||||
self.popularity.setChecked(config['sort_by_popularity'])
|
self.popularity.setChecked(config['sort_by_popularity'])
|
||||||
self.connect(self.popularity, SIGNAL('stateChanged(int)'), self.sort_changed)
|
self.connect(self.popularity, SIGNAL('stateChanged(int)'), self.sort_changed)
|
||||||
|
self.connect(self.restriction, SIGNAL('activated(const QString&)'), self.search_restriction_set)
|
||||||
self.need_refresh.connect(self.recount, type=Qt.QueuedConnection)
|
self.need_refresh.connect(self.recount, type=Qt.QueuedConnection)
|
||||||
db.add_listener(self.database_changed)
|
db.add_listener(self.database_changed)
|
||||||
|
self.saved_searches_changed(recount=False)
|
||||||
|
|
||||||
|
def create_tag_category(self, name, tag_list):
|
||||||
|
self._model.create_tag_category(name, tag_list)
|
||||||
|
self.recount()
|
||||||
|
|
||||||
def database_changed(self, event, ids):
|
def database_changed(self, event, ids):
|
||||||
self.need_refresh.emit()
|
self.need_refresh.emit()
|
||||||
@ -48,6 +58,19 @@ class TagsView(QTreeView):
|
|||||||
def sort_changed(self, state):
|
def sort_changed(self, state):
|
||||||
config.set('sort_by_popularity', state == Qt.Checked)
|
config.set('sort_by_popularity', state == Qt.Checked)
|
||||||
self.model().refresh()
|
self.model().refresh()
|
||||||
|
# self.search_restriction_set()
|
||||||
|
|
||||||
|
def search_restriction_set(self, s):
|
||||||
|
self.clear()
|
||||||
|
if len(s) == 0:
|
||||||
|
self.search_restriction = ''
|
||||||
|
else:
|
||||||
|
self.search_restriction = unicode(s)
|
||||||
|
self.model().set_search_restriction(self.search_restriction)
|
||||||
|
self.recount()
|
||||||
|
self.emit(SIGNAL('restriction_set(PyQt_PyObject)'), self.search_restriction)
|
||||||
|
self.emit(SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
|
||||||
|
self._model.tokens(), self.match_all)
|
||||||
|
|
||||||
def toggle(self, index):
|
def toggle(self, index):
|
||||||
modifiers = int(QApplication.keyboardModifiers())
|
modifiers = int(QApplication.keyboardModifiers())
|
||||||
@ -59,6 +82,20 @@ class TagsView(QTreeView):
|
|||||||
def clear(self):
|
def clear(self):
|
||||||
self.model().clear_state()
|
self.model().clear_state()
|
||||||
|
|
||||||
|
def saved_searches_changed(self, recount=True):
|
||||||
|
p = prefs['saved_searches'].keys()
|
||||||
|
p.sort()
|
||||||
|
t = self.restriction.currentText()
|
||||||
|
self.restriction.clear() # rebuild the restrictions combobox using current saved searches
|
||||||
|
self.restriction.addItem('')
|
||||||
|
for s in p:
|
||||||
|
self.restriction.addItem(s)
|
||||||
|
if t in p: # redo the current restriction, if there was one
|
||||||
|
self.restriction.setCurrentIndex(self.restriction.findText(t))
|
||||||
|
self.search_restriction_set(t)
|
||||||
|
if recount:
|
||||||
|
self.recount()
|
||||||
|
|
||||||
def recount(self, *args):
|
def recount(self, *args):
|
||||||
ci = self.currentIndex()
|
ci = self.currentIndex()
|
||||||
if not ci.isValid():
|
if not ci.isValid():
|
||||||
@ -74,13 +111,22 @@ class TagsView(QTreeView):
|
|||||||
self.setCurrentIndex(idx)
|
self.setCurrentIndex(idx)
|
||||||
self.scrollTo(idx, QTreeView.PositionAtCenter)
|
self.scrollTo(idx, QTreeView.PositionAtCenter)
|
||||||
|
|
||||||
|
'''
|
||||||
|
If the number of user categories changed, or if custom columns have come or gone,
|
||||||
|
we must rebuild the model. Reason: it is much easier to do that than to reconstruct
|
||||||
|
the browser tree.
|
||||||
|
'''
|
||||||
|
def set_new_model(self):
|
||||||
|
self._model = TagsModel(self.db, parent=self)
|
||||||
|
self.setModel(self._model)
|
||||||
|
|
||||||
class TagTreeItem(object):
|
class TagTreeItem(object):
|
||||||
|
|
||||||
CATEGORY = 0
|
CATEGORY = 0
|
||||||
TAG = 1
|
TAG = 1
|
||||||
ROOT = 2
|
ROOT = 2
|
||||||
|
|
||||||
def __init__(self, data=None, tag=None, category_icon=None, icon_map=None, parent=None):
|
def __init__(self, data=None, category_icon=None, icon_map=None, parent=None):
|
||||||
self.parent = parent
|
self.parent = parent
|
||||||
self.children = []
|
self.children = []
|
||||||
if self.parent is not None:
|
if self.parent is not None:
|
||||||
@ -96,13 +142,14 @@ class TagTreeItem(object):
|
|||||||
self.bold_font.setBold(True)
|
self.bold_font.setBold(True)
|
||||||
self.bold_font = QVariant(self.bold_font)
|
self.bold_font = QVariant(self.bold_font)
|
||||||
elif self.type == self.TAG:
|
elif self.type == self.TAG:
|
||||||
self.tag, self.icon_map = data, list(map(QVariant, icon_map))
|
icon_map[0] = data.icon
|
||||||
|
self.tag, self.icon_state_map = data, list(map(QVariant, icon_map))
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
if self.type == self.ROOT:
|
if self.type == self.ROOT:
|
||||||
return 'ROOT'
|
return 'ROOT'
|
||||||
if self.type == self.CATEGORY:
|
if self.type == self.CATEGORY:
|
||||||
return 'CATEGORY:'+self.name+':%d'%len(self.children)
|
return 'CATEGORY:'+str(QVariant.toString(self.name))+':%d'%len(self.children)
|
||||||
return 'TAG:'+self.tag.name
|
return 'TAG:'+self.tag.name
|
||||||
|
|
||||||
def row(self):
|
def row(self):
|
||||||
@ -137,7 +184,7 @@ class TagTreeItem(object):
|
|||||||
else:
|
else:
|
||||||
return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
|
return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
|
||||||
if role == Qt.DecorationRole:
|
if role == Qt.DecorationRole:
|
||||||
return self.icon_map[self.tag.state]
|
return self.icon_state_map[self.tag.state]
|
||||||
if role == Qt.ToolTipRole and self.tag.tooltip:
|
if role == Qt.ToolTipRole and self.tag.tooltip:
|
||||||
return QVariant(self.tag.tooltip)
|
return QVariant(self.tag.tooltip)
|
||||||
return NONE
|
return NONE
|
||||||
@ -148,38 +195,100 @@ class TagTreeItem(object):
|
|||||||
|
|
||||||
|
|
||||||
class TagsModel(QAbstractItemModel):
|
class TagsModel(QAbstractItemModel):
|
||||||
categories = [_('Authors'), _('Series'), _('Formats'), _('Publishers'), _('News'), _('Tags'), _('Searches')]
|
categories_orig = [_('Authors'), _('Series'), _('Formats'), _('Publishers'), _('News'), _('All tags')]
|
||||||
row_map = ['author', 'series', 'format', 'publisher', 'news', 'tag', 'search']
|
row_map_orig = ['author', 'series', 'format', 'publisher', 'news', 'tag']
|
||||||
|
tags_categories_start= 5
|
||||||
|
search_keys=['search', _('Searches')]
|
||||||
|
|
||||||
def __init__(self, db, parent=None):
|
def __init__(self, db, parent=None):
|
||||||
QAbstractItemModel.__init__(self, parent)
|
QAbstractItemModel.__init__(self, parent)
|
||||||
self.cmap = tuple(map(QIcon, [I('user_profile.svg'),
|
self.cat_icon_map_orig = list(map(QIcon, [I('user_profile.svg'),
|
||||||
I('series.svg'), I('book.svg'), I('publisher.png'),
|
I('series.svg'), I('book.svg'), I('publisher.png'),
|
||||||
I('news.svg'), I('tags.svg'), I('search.svg')]))
|
I('news.svg'), I('tags.svg')]))
|
||||||
self.icon_map = [QIcon(), QIcon(I('plus.svg')),
|
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
|
||||||
QIcon(I('minus.svg'))]
|
self.custcol_icon = QIcon(I('column.svg'))
|
||||||
|
self.search_icon = QIcon(I('search.svg'))
|
||||||
|
self.usercat_icon = QIcon(I('drawer.svg'))
|
||||||
|
self.label_to_icon_map = dict(map(None, self.row_map_orig, self.cat_icon_map_orig))
|
||||||
|
self.label_to_icon_map['*custom'] = self.custcol_icon
|
||||||
self.db = db
|
self.db = db
|
||||||
|
self.search_restriction = ''
|
||||||
|
self.user_categories = {}
|
||||||
self.ignore_next_search = 0
|
self.ignore_next_search = 0
|
||||||
|
data = self.get_node_tree(config['sort_by_popularity'])
|
||||||
self.root_item = TagTreeItem()
|
self.root_item = TagTreeItem()
|
||||||
data = self.db.get_categories(config['sort_by_popularity'])
|
|
||||||
data['search'] = self.get_search_nodes()
|
|
||||||
|
|
||||||
for i, r in enumerate(self.row_map):
|
for i, r in enumerate(self.row_map):
|
||||||
c = TagTreeItem(parent=self.root_item,
|
c = TagTreeItem(parent=self.root_item,
|
||||||
data=self.categories[i], category_icon=self.cmap[i])
|
data=self.categories[i], category_icon=self.cat_icon_map[i])
|
||||||
for tag in data[r]:
|
for tag in data[r]:
|
||||||
TagTreeItem(parent=c, data=tag, icon_map=self.icon_map)
|
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
|
||||||
|
|
||||||
|
def set_search_restriction(self, s):
|
||||||
|
self.search_restriction = s
|
||||||
|
|
||||||
def get_search_nodes(self):
|
def get_node_tree(self, sort):
|
||||||
|
self.row_map = []
|
||||||
|
self.categories = []
|
||||||
|
# strip the icons after the 'standard' categories. We will put them back later
|
||||||
|
self.cat_icon_map = self.cat_icon_map_orig[:self.tags_categories_start-len(self.row_map_orig)]
|
||||||
|
self.user_categories = dict.copy(config['user_categories'])
|
||||||
|
column_map = config['column_map']
|
||||||
|
|
||||||
|
for i in range(0, self.tags_categories_start): # First the standard categories
|
||||||
|
self.row_map.append(self.row_map_orig[i])
|
||||||
|
self.categories.append(self.categories_orig[i])
|
||||||
|
if len(self.search_restriction):
|
||||||
|
data = self.db.get_categories(sort_on_count=sort, icon_map=self.label_to_icon_map,
|
||||||
|
ids=self.db.search(self.search_restriction, return_matches=True))
|
||||||
|
else:
|
||||||
|
data = self.db.get_categories(sort_on_count=sort, icon_map=self.label_to_icon_map)
|
||||||
|
|
||||||
|
for c in data: # now the custom columns
|
||||||
|
if c not in self.row_map_orig and c in column_map:
|
||||||
|
self.row_map.append(c)
|
||||||
|
self.categories.append(self.db.custom_column_label_map[c]['name'])
|
||||||
|
self.cat_icon_map.append(self.custcol_icon)
|
||||||
|
|
||||||
|
# Now do the user-defined categories. There is a time/space tradeoff here.
|
||||||
|
# By converting the tags into a map, we can do the verification in the category
|
||||||
|
# loop much faster, at the cost of duplicating the categories lists.
|
||||||
|
taglist = {}
|
||||||
|
for c in self.row_map_orig:
|
||||||
|
taglist[c] = dict(map(lambda t:(t.name if c != 'author' else t.name.replace('|', ','), t), data[c]))
|
||||||
|
|
||||||
|
for c in self.user_categories:
|
||||||
|
l = []
|
||||||
|
for (name,label,ign) in self.user_categories[c]:
|
||||||
|
if name in taglist[label]: # use same node as the complete category
|
||||||
|
l.append(taglist[label][name])
|
||||||
|
# else: do nothing, to eliminate nodes that have zero counts
|
||||||
|
if config['sort_by_popularity']:
|
||||||
|
data[c+'*'] = sorted(l, cmp=(lambda x, y: cmp(x.count, y.count)))
|
||||||
|
else:
|
||||||
|
data[c+'*'] = sorted(l, cmp=(lambda x, y: cmp(x.name.lower(), y.name.lower())))
|
||||||
|
self.row_map.append(c+'*')
|
||||||
|
self.categories.append(c)
|
||||||
|
self.cat_icon_map.append(self.usercat_icon)
|
||||||
|
|
||||||
|
# Now the rest of the normal tag categories
|
||||||
|
for i in range(self.tags_categories_start, len(self.row_map_orig)):
|
||||||
|
self.row_map.append(self.row_map_orig[i])
|
||||||
|
self.categories.append(self.categories_orig[i])
|
||||||
|
self.cat_icon_map.append(self.cat_icon_map_orig[i])
|
||||||
|
data['search'] = self.get_search_nodes(self.search_icon) # Add the search category
|
||||||
|
self.row_map.append(self.search_keys[0])
|
||||||
|
self.categories.append(self.search_keys[1])
|
||||||
|
self.cat_icon_map.append(self.search_icon)
|
||||||
|
return data
|
||||||
|
|
||||||
|
def get_search_nodes(self, icon):
|
||||||
l = []
|
l = []
|
||||||
for i in saved_searches.names():
|
for i in saved_searches.names():
|
||||||
l.append(Tag(i, tooltip=saved_searches.lookup(i)))
|
l.append(Tag(i, tooltip=saved_searches.lookup(i), icon=icon))
|
||||||
return l
|
return l
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
data = self.db.get_categories(config['sort_by_popularity'])
|
data = self.get_node_tree(config['sort_by_popularity']) # get category data
|
||||||
data['search'] = self.get_search_nodes()
|
|
||||||
for i, r in enumerate(self.row_map):
|
for i, r in enumerate(self.row_map):
|
||||||
category = self.root_item.children[i]
|
category = self.root_item.children[i]
|
||||||
names = [t.tag.name for t in category.children]
|
names = [t.tag.name for t in category.children]
|
||||||
@ -194,10 +303,8 @@ class TagsModel(QAbstractItemModel):
|
|||||||
if len(data[r]) > 0:
|
if len(data[r]) > 0:
|
||||||
self.beginInsertRows(category_index, 0, len(data[r])-1)
|
self.beginInsertRows(category_index, 0, len(data[r])-1)
|
||||||
for tag in data[r]:
|
for tag in data[r]:
|
||||||
if r == 'author':
|
|
||||||
tag.name = tag.name.replace('|', ',')
|
|
||||||
tag.state = state_map.get(tag.name, 0)
|
tag.state = state_map.get(tag.name, 0)
|
||||||
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_map)
|
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
|
||||||
self.endInsertRows()
|
self.endInsertRows()
|
||||||
|
|
||||||
def columnCount(self, parent):
|
def columnCount(self, parent):
|
||||||
@ -273,16 +380,20 @@ class TagsModel(QAbstractItemModel):
|
|||||||
return len(parent_item.children)
|
return len(parent_item.children)
|
||||||
|
|
||||||
def reset_all_states(self, except_=None):
|
def reset_all_states(self, except_=None):
|
||||||
|
update_list = []
|
||||||
for i in xrange(self.rowCount(QModelIndex())):
|
for i in xrange(self.rowCount(QModelIndex())):
|
||||||
category_index = self.index(i, 0, QModelIndex())
|
category_index = self.index(i, 0, QModelIndex())
|
||||||
for j in xrange(self.rowCount(category_index)):
|
for j in xrange(self.rowCount(category_index)):
|
||||||
tag_index = self.index(j, 0, category_index)
|
tag_index = self.index(j, 0, category_index)
|
||||||
tag_item = tag_index.internalPointer()
|
tag_item = tag_index.internalPointer()
|
||||||
if tag_item is except_:
|
|
||||||
continue
|
|
||||||
tag = tag_item.tag
|
tag = tag_item.tag
|
||||||
if tag.state != 0:
|
if tag is except_:
|
||||||
|
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
|
||||||
|
tag_index, tag_index)
|
||||||
|
continue
|
||||||
|
if tag.state != 0 or tag in update_list:
|
||||||
tag.state = 0
|
tag.state = 0
|
||||||
|
update_list.append(tag)
|
||||||
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
|
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'),
|
||||||
tag_index, tag_index)
|
tag_index, tag_index)
|
||||||
|
|
||||||
@ -299,9 +410,9 @@ class TagsModel(QAbstractItemModel):
|
|||||||
if not index.isValid(): return False
|
if not index.isValid(): return False
|
||||||
item = index.internalPointer()
|
item = index.internalPointer()
|
||||||
if item.type == TagTreeItem.TAG:
|
if item.type == TagTreeItem.TAG:
|
||||||
if exclusive:
|
|
||||||
self.reset_all_states(except_=item)
|
|
||||||
item.toggle()
|
item.toggle()
|
||||||
|
if exclusive:
|
||||||
|
self.reset_all_states(except_=item.tag)
|
||||||
self.ignore_next_search = 2
|
self.ignore_next_search = 2
|
||||||
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), index, index)
|
self.emit(SIGNAL('dataChanged(QModelIndex,QModelIndex)'), index, index)
|
||||||
return True
|
return True
|
||||||
@ -309,14 +420,19 @@ class TagsModel(QAbstractItemModel):
|
|||||||
|
|
||||||
def tokens(self):
|
def tokens(self):
|
||||||
ans = []
|
ans = []
|
||||||
|
tags_seen = []
|
||||||
for i, key in enumerate(self.row_map):
|
for i, key in enumerate(self.row_map):
|
||||||
|
if key.endswith('*'): # User category, so skip it. The tag will be marked in its real category
|
||||||
|
continue
|
||||||
category_item = self.root_item.children[i]
|
category_item = self.root_item.children[i]
|
||||||
for tag_item in category_item.children:
|
for tag_item in category_item.children:
|
||||||
tag = tag_item.tag
|
tag = tag_item.tag
|
||||||
category = key if key != 'news' else 'tag'
|
|
||||||
if tag.state > 0:
|
if tag.state > 0:
|
||||||
prefix = ' not ' if tag.state == 2 else ''
|
prefix = ' not ' if tag.state == 2 else ''
|
||||||
|
category = key if key != 'news' else 'tag'
|
||||||
|
if category == 'tag':
|
||||||
|
if tag.name in tags_seen:
|
||||||
|
continue
|
||||||
|
tags_seen.append(tag.name)
|
||||||
ans.append('%s%s:"=%s"'%(prefix, category, tag.name))
|
ans.append('%s%s:"=%s"'%(prefix, category, tag.name))
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
|
@ -60,6 +60,9 @@ from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
|
|||||||
from calibre.library.database2 import LibraryDatabase2
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
from calibre.library.caches import CoverCache
|
from calibre.library.caches import CoverCache
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
|
from calibre.gui2.dialogs.tag_categories import TagCategories
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
class SaveMenu(QMenu):
|
class SaveMenu(QMenu):
|
||||||
|
|
||||||
@ -126,8 +129,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
pixmap_to_data(pixmap))
|
pixmap_to_data(pixmap))
|
||||||
|
|
||||||
def __init__(self, listener, opts, actions, parent=None):
|
def __init__(self, listener, opts, actions, parent=None):
|
||||||
|
self.last_time = datetime.now()
|
||||||
self.preferences_action, self.quit_action = actions
|
self.preferences_action, self.quit_action = actions
|
||||||
self.spare_servers = []
|
self.spare_servers = []
|
||||||
|
self.must_restart_before_config = False
|
||||||
MainWindow.__init__(self, opts, parent)
|
MainWindow.__init__(self, opts, parent)
|
||||||
# Initialize fontconfig in a separate thread as this can be a lengthy
|
# Initialize fontconfig in a separate thread as this can be a lengthy
|
||||||
# process if run for the first time on this machine
|
# process if run for the first time on this machine
|
||||||
@ -143,6 +148,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
self.setWindowTitle(__appname__)
|
self.setWindowTitle(__appname__)
|
||||||
|
|
||||||
|
self.restriction_count_of_books_in_view = 0
|
||||||
|
self.restriction_count_of_books_in_library = 0
|
||||||
|
self.restriction_in_effect = False
|
||||||
self.search.initialize('main_search_history', colorize=True,
|
self.search.initialize('main_search_history', colorize=True,
|
||||||
help_text=_('Search (For Advanced Search click the button to the left)'))
|
help_text=_('Search (For Advanced Search click the button to the left)'))
|
||||||
self.connect(self.clear_button, SIGNAL('clicked()'), self.search_clear)
|
self.connect(self.clear_button, SIGNAL('clicked()'), self.search_clear)
|
||||||
@ -501,6 +509,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.search_done)),
|
self.search_done)),
|
||||||
('connect_to_book_display',
|
('connect_to_book_display',
|
||||||
(self.status_bar.book_info.show_data,)),
|
(self.status_bar.book_info.show_data,)),
|
||||||
|
('connect_to_restriction_set',
|
||||||
|
(self.tags_view,)),
|
||||||
]:
|
]:
|
||||||
for view in (self.library_view, self.memory_view, self.card_a_view, self.card_b_view):
|
for view in (self.library_view, self.memory_view, self.card_a_view, self.card_b_view):
|
||||||
getattr(view, func)(*args)
|
getattr(view, func)(*args)
|
||||||
@ -540,8 +550,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
db = LibraryDatabase2(self.library_path)
|
db = LibraryDatabase2(self.library_path)
|
||||||
self.library_view.set_database(db)
|
self.library_view.set_database(db)
|
||||||
prefs['library_path'] = self.library_path
|
prefs['library_path'] = self.library_path
|
||||||
self.library_view.sortByColumn(*dynamic.get('sort_column',
|
self.library_view.restore_sort_at_startup(dynamic.get('sort_history', [('timestamp', Qt.DescendingOrder)]))
|
||||||
('timestamp', Qt.DescendingOrder)))
|
|
||||||
if not self.library_view.restore_column_widths():
|
if not self.library_view.restore_column_widths():
|
||||||
self.library_view.resizeColumnsToContents()
|
self.library_view.resizeColumnsToContents()
|
||||||
self.search.setFocus(Qt.OtherFocusReason)
|
self.search.setFocus(Qt.OtherFocusReason)
|
||||||
@ -551,10 +560,20 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.tags_view.setVisible(False)
|
self.tags_view.setVisible(False)
|
||||||
self.tag_match.setVisible(False)
|
self.tag_match.setVisible(False)
|
||||||
self.popularity.setVisible(False)
|
self.popularity.setVisible(False)
|
||||||
self.tags_view.set_database(db, self.tag_match, self.popularity)
|
self.restriction_label.setVisible(False)
|
||||||
|
self.edit_categories.setVisible(False)
|
||||||
|
self.search_restriction.setVisible(False)
|
||||||
|
self.connect(self.edit_categories, SIGNAL('clicked()'), self.do_edit_categories)
|
||||||
|
self.tags_view.set_database(db, self.tag_match, self.popularity, self.search_restriction)
|
||||||
self.connect(self.tags_view,
|
self.connect(self.tags_view,
|
||||||
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
|
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
|
||||||
self.search.search_from_tags)
|
self.search.search_from_tags)
|
||||||
|
self.connect(self.tags_view,
|
||||||
|
SIGNAL('restriction_set(PyQt_PyObject)'),
|
||||||
|
self.saved_search.clear_to_help)
|
||||||
|
self.connect(self.tags_view,
|
||||||
|
SIGNAL('restriction_set(PyQt_PyObject)'),
|
||||||
|
self.mark_restriction_set)
|
||||||
self.connect(self.tags_view,
|
self.connect(self.tags_view,
|
||||||
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
|
SIGNAL('tags_marked(PyQt_PyObject, PyQt_PyObject)'),
|
||||||
self.saved_search.clear_to_help)
|
self.saved_search.clear_to_help)
|
||||||
@ -567,8 +586,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
SIGNAL('count_changed(int)'), self.location_view.count_changed)
|
SIGNAL('count_changed(int)'), self.location_view.count_changed)
|
||||||
self.connect(self.library_view.model(), SIGNAL('count_changed(int)'),
|
self.connect(self.library_view.model(), SIGNAL('count_changed(int)'),
|
||||||
self.tags_view.recount, Qt.QueuedConnection)
|
self.tags_view.recount, Qt.QueuedConnection)
|
||||||
self.connect(self.search, SIGNAL('cleared()'), self.tags_view_clear)
|
self.connect(self.library_view.model(), SIGNAL('count_changed(int)'),
|
||||||
self.connect(self.saved_search, SIGNAL('changed()'), self.tags_view.recount, Qt.QueuedConnection)
|
self.restriction_count_changed, Qt.QueuedConnection)
|
||||||
|
self.connect(self.search, SIGNAL('cleared()'), self.search_box_cleared)
|
||||||
|
self.connect(self.saved_search, SIGNAL('changed()'), self.tags_view.saved_searches_changed, Qt.QueuedConnection)
|
||||||
if not gprefs.get('quick_start_guide_added', False):
|
if not gprefs.get('quick_start_guide_added', False):
|
||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
|
mi = MetaInformation(_('Calibre Quick Start Guide'), ['John Schember'])
|
||||||
@ -618,7 +639,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.resize(self.width(), self._calculated_available_height)
|
self.resize(self.width(), self._calculated_available_height)
|
||||||
self.search.setMaximumWidth(self.width()-150)
|
self.search.setMaximumWidth(self.width()-150)
|
||||||
|
|
||||||
|
|
||||||
if config['autolaunch_server']:
|
if config['autolaunch_server']:
|
||||||
from calibre.library.server import start_threaded_server
|
from calibre.library.server import start_threaded_server
|
||||||
from calibre.library import server_config
|
from calibre.library import server_config
|
||||||
@ -658,6 +678,12 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
height = v.rowHeight(0)
|
height = v.rowHeight(0)
|
||||||
self.library_view.verticalHeader().setDefaultSectionSize(height)
|
self.library_view.verticalHeader().setDefaultSectionSize(height)
|
||||||
|
|
||||||
|
def do_edit_categories(self):
|
||||||
|
d = TagCategories(self, self.library_view.model().db)
|
||||||
|
d.exec_()
|
||||||
|
if d.result() == d.Accepted:
|
||||||
|
self.tags_view.set_new_model()
|
||||||
|
self.tags_view.recount()
|
||||||
|
|
||||||
def resizeEvent(self, ev):
|
def resizeEvent(self, ev):
|
||||||
MainWindow.resizeEvent(self, ev)
|
MainWindow.resizeEvent(self, ev)
|
||||||
@ -809,23 +835,68 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
self.tags_view.setVisible(True)
|
self.tags_view.setVisible(True)
|
||||||
self.tag_match.setVisible(True)
|
self.tag_match.setVisible(True)
|
||||||
self.popularity.setVisible(True)
|
self.popularity.setVisible(True)
|
||||||
|
self.restriction_label.setVisible(True)
|
||||||
|
self.edit_categories.setVisible(True)
|
||||||
|
self.search_restriction.setVisible(True)
|
||||||
self.tags_view.setFocus(Qt.OtherFocusReason)
|
self.tags_view.setFocus(Qt.OtherFocusReason)
|
||||||
else:
|
else:
|
||||||
self.tags_view.setVisible(False)
|
self.tags_view.setVisible(False)
|
||||||
self.tag_match.setVisible(False)
|
self.tag_match.setVisible(False)
|
||||||
self.popularity.setVisible(False)
|
self.popularity.setVisible(False)
|
||||||
|
self.restriction_label.setVisible(False)
|
||||||
|
self.edit_categories.setVisible(False)
|
||||||
|
self.search_restriction.setVisible(False)
|
||||||
|
|
||||||
def tags_view_clear(self):
|
'''
|
||||||
self.search_count.setText(_("(all books)"))
|
Handling of the count of books in a restricted view requires that
|
||||||
|
we capture the count after the initial restriction search. To so this,
|
||||||
|
we require that the restriction_set signal be issued before the search signal,
|
||||||
|
so that when the search_done happens and the count is displayed,
|
||||||
|
we can grab the count. This works because the search box is cleared
|
||||||
|
when a restriction is set, so that first search will find all books.
|
||||||
|
|
||||||
|
Adding and deleting books creates another complexity. When added, they are
|
||||||
|
displayed regardless of whether they match the restriction. However, if they
|
||||||
|
do not, they are removed at the next search. The counts must take this
|
||||||
|
behavior into effect.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def restriction_count_changed(self, c):
|
||||||
|
self.restriction_count_of_books_in_view += c - self.restriction_count_of_books_in_library
|
||||||
|
self.restriction_count_of_books_in_library = c
|
||||||
|
if self.restriction_in_effect:
|
||||||
|
self.set_number_of_books_shown(all='not used', compute_count=False)
|
||||||
|
|
||||||
|
def mark_restriction_set(self, r):
|
||||||
|
self.restriction_in_effect = False if r is None or not r else True
|
||||||
|
|
||||||
|
def set_number_of_books_shown(self, all, compute_count):
|
||||||
|
if self.restriction_in_effect:
|
||||||
|
if compute_count:
|
||||||
|
self.restriction_count_of_books_in_view = self.current_view().row_count()
|
||||||
|
t = _("({0} of {1})").format(self.current_view().row_count(),
|
||||||
|
self.restriction_count_of_books_in_view)
|
||||||
|
self.search_count.setStyleSheet('QLabel { background-color: yellow; }')
|
||||||
|
else: # No restriction
|
||||||
|
if all == 'yes':
|
||||||
|
t = _("(all books)")
|
||||||
|
else:
|
||||||
|
t = _("({0} of all)").format(self.current_view().row_count())
|
||||||
|
self.search_count.setStyleSheet('QLabel { background-color: white; }')
|
||||||
|
self.search_count.setText(t)
|
||||||
|
|
||||||
|
def search_box_cleared(self):
|
||||||
|
self.set_number_of_books_shown(all='yes', compute_count=True)
|
||||||
self.tags_view.clear()
|
self.tags_view.clear()
|
||||||
|
self.saved_search.clear_to_help()
|
||||||
|
|
||||||
def search_clear(self):
|
def search_clear(self):
|
||||||
self.search_count.setText(_("(all books)"))
|
self.set_number_of_books_shown(all='yes', compute_count=True)
|
||||||
self.search.clear()
|
self.search.clear()
|
||||||
|
|
||||||
def search_done(self, view, ok):
|
def search_done(self, view, ok):
|
||||||
if view is self.current_view():
|
if view is self.current_view():
|
||||||
self.search_count.setText(_("(%d found)") % self.current_view().row_count())
|
self.set_number_of_books_shown(all='no', compute_count=False)
|
||||||
self.search.search_done(ok)
|
self.search.search_done(ok)
|
||||||
|
|
||||||
def sync_cf_to_listview(self, current, previous):
|
def sync_cf_to_listview(self, current, previous):
|
||||||
@ -2174,7 +2245,12 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
_('Cannot configure while there are running jobs.'))
|
_('Cannot configure while there are running jobs.'))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
return
|
return
|
||||||
d = ConfigDialog(self, self.library_view.model().db,
|
if self.must_restart_before_config:
|
||||||
|
d = error_dialog(self, _('Cannot configure'),
|
||||||
|
_('Cannot configure before calibre is restarted.'))
|
||||||
|
d.exec_()
|
||||||
|
return
|
||||||
|
d = ConfigDialog(self, self.library_view.model(),
|
||||||
server=self.content_server)
|
server=self.content_server)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
self.content_server = d.server
|
self.content_server = d.server
|
||||||
@ -2189,15 +2265,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
_('Save only %s format to disk')%
|
_('Save only %s format to disk')%
|
||||||
prefs['output_format'].upper())
|
prefs['output_format'].upper())
|
||||||
self.library_view.model().read_config()
|
self.library_view.model().read_config()
|
||||||
|
self.library_view.model().refresh()
|
||||||
|
self.library_view.model().research()
|
||||||
|
self.tags_view.set_new_model() # in case columns changed
|
||||||
|
self.tags_view.recount()
|
||||||
self.create_device_menu()
|
self.create_device_menu()
|
||||||
|
|
||||||
|
|
||||||
if not patheq(self.library_path, d.database_location):
|
if not patheq(self.library_path, d.database_location):
|
||||||
newloc = d.database_location
|
newloc = d.database_location
|
||||||
move_library(self.library_path, newloc, self,
|
move_library(self.library_path, newloc, self,
|
||||||
self.library_moved)
|
self.library_moved)
|
||||||
|
|
||||||
|
|
||||||
def library_moved(self, newloc):
|
def library_moved(self, newloc):
|
||||||
if newloc is None: return
|
if newloc is None: return
|
||||||
db = LibraryDatabase2(newloc)
|
db = LibraryDatabase2(newloc)
|
||||||
@ -2374,7 +2452,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
|
|||||||
|
|
||||||
def write_settings(self):
|
def write_settings(self):
|
||||||
config.set('main_window_geometry', self.saveGeometry())
|
config.set('main_window_geometry', self.saveGeometry())
|
||||||
dynamic.set('sort_column', self.library_view.model().sorted_on)
|
dynamic.set('sort_history', self.library_view.model().sort_history)
|
||||||
dynamic.set('tag_view_visible', self.tags_view.isVisible())
|
dynamic.set('tag_view_visible', self.tags_view.isVisible())
|
||||||
dynamic.set('cover_flow_visible', self.cover_flow.isVisible())
|
dynamic.set('cover_flow_visible', self.cover_flow.isVisible())
|
||||||
self.library_view.write_settings()
|
self.library_view.write_settings()
|
||||||
|
@ -8,12 +8,15 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import collections, glob, os, re, itertools, functools
|
import collections, glob, os, re, itertools, functools
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
from PyQt4.QtCore import QThread, QReadWriteLock
|
from PyQt4.QtCore import QThread, QReadWriteLock
|
||||||
from PyQt4.QtGui import QImage
|
from PyQt4.QtGui import QImage
|
||||||
|
|
||||||
|
from calibre.utils.config import tweaks, prefs
|
||||||
|
from calibre.utils.date import parse_date, now
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
from calibre.utils.date import parse_date
|
from calibre.utils.pyparsing import ParseException
|
||||||
|
|
||||||
class CoverCache(QThread):
|
class CoverCache(QThread):
|
||||||
|
|
||||||
@ -146,6 +149,14 @@ class ResultCache(SearchQueryParser):
|
|||||||
'''
|
'''
|
||||||
Stores sorted and filtered metadata in memory.
|
Stores sorted and filtered metadata in memory.
|
||||||
'''
|
'''
|
||||||
|
def __init__(self, FIELD_MAP, cc_label_map):
|
||||||
|
self.FIELD_MAP = FIELD_MAP
|
||||||
|
self.custom_column_label_map = cc_label_map
|
||||||
|
self._map = self._map_filtered = self._data = []
|
||||||
|
self.first_sort = True
|
||||||
|
self.search_restriction = ''
|
||||||
|
SearchQueryParser.__init__(self, [c for c in cc_label_map])
|
||||||
|
self.build_relop_dict()
|
||||||
|
|
||||||
def build_relop_dict(self):
|
def build_relop_dict(self):
|
||||||
'''
|
'''
|
||||||
@ -194,13 +205,6 @@ class ResultCache(SearchQueryParser):
|
|||||||
self.search_relops = {'=':[1, relop_eq], '>':[1, relop_gt], '<':[1, relop_lt], \
|
self.search_relops = {'=':[1, relop_eq], '>':[1, relop_gt], '<':[1, relop_lt], \
|
||||||
'!=':[2, relop_ne], '>=':[2, relop_ge], '<=':[2, relop_le]}
|
'!=':[2, relop_ne], '>=':[2, relop_ge], '<=':[2, relop_le]}
|
||||||
|
|
||||||
def __init__(self, FIELD_MAP):
|
|
||||||
self.FIELD_MAP = FIELD_MAP
|
|
||||||
self._map = self._map_filtered = self._data = []
|
|
||||||
self.first_sort = True
|
|
||||||
SearchQueryParser.__init__(self)
|
|
||||||
self.build_relop_dict()
|
|
||||||
|
|
||||||
def __getitem__(self, row):
|
def __getitem__(self, row):
|
||||||
return self._data[self._map_filtered[row]]
|
return self._data[self._map_filtered[row]]
|
||||||
|
|
||||||
@ -214,30 +218,63 @@ class ResultCache(SearchQueryParser):
|
|||||||
def universal_set(self):
|
def universal_set(self):
|
||||||
return set([i[0] for i in self._data if i is not None])
|
return set([i[0] for i in self._data if i is not None])
|
||||||
|
|
||||||
|
def get_matches_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):
|
def get_matches(self, location, query):
|
||||||
matches = set([])
|
matches = set([])
|
||||||
if query and query.strip():
|
if query and query.strip():
|
||||||
location = location.lower().strip()
|
location = location.lower().strip()
|
||||||
|
|
||||||
### take care of dates special case
|
### take care of dates special case
|
||||||
if location in ('pubdate', 'date'):
|
if (location in ('pubdate', 'date')) or \
|
||||||
if len(query) < 2:
|
((location in self.custom_column_label_map) and \
|
||||||
return matches
|
self.custom_column_label_map[location]['datatype'] == 'datetime'):
|
||||||
relop = None
|
return self.get_matches_dates(location, query)
|
||||||
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
|
|
||||||
|
|
||||||
### everything else
|
### everything else
|
||||||
matchkind = CONTAINS_MATCH
|
matchkind = CONTAINS_MATCH
|
||||||
@ -257,19 +294,39 @@ class ResultCache(SearchQueryParser):
|
|||||||
query = query.decode('utf-8')
|
query = query.decode('utf-8')
|
||||||
if location in ('tag', 'author', 'format', 'comment'):
|
if location in ('tag', 'author', 'format', 'comment'):
|
||||||
location += 's'
|
location += 's'
|
||||||
|
|
||||||
all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series', 'formats', 'isbn', 'rating', 'cover')
|
all = ('title', 'authors', 'publisher', 'tags', 'comments', 'series', 'formats', 'isbn', 'rating', 'cover')
|
||||||
MAP = {}
|
MAP = {}
|
||||||
for x in all:
|
|
||||||
|
for x in all: # get the db columns for the standard searchables
|
||||||
MAP[x] = self.FIELD_MAP[x]
|
MAP[x] = self.FIELD_MAP[x]
|
||||||
|
IS_CUSTOM = []
|
||||||
|
for x in range(len(self.FIELD_MAP)): # build a list containing '' the size of FIELD_MAP
|
||||||
|
IS_CUSTOM.append('')
|
||||||
|
IS_CUSTOM[self.FIELD_MAP['rating']] = 'rating' # normal and custom ratings columns use the same code
|
||||||
|
for x in self.custom_column_label_map: # add custom columns to MAP. Put the column's type into IS_CUSTOM
|
||||||
|
if self.custom_column_label_map[x]['datatype'] != "datetime":
|
||||||
|
MAP[x] = self.FIELD_MAP[self.custom_column_label_map[x]['num']]
|
||||||
|
IS_CUSTOM[MAP[x]] = self.custom_column_label_map[x]['datatype']
|
||||||
|
|
||||||
EXCLUDE_FIELDS = [MAP['rating'], MAP['cover']]
|
EXCLUDE_FIELDS = [MAP['rating'], MAP['cover']]
|
||||||
SPLITABLE_FIELDS = [MAP['authors'], MAP['tags'], MAP['formats']]
|
SPLITABLE_FIELDS = [MAP['authors'], MAP['tags'], MAP['formats']]
|
||||||
|
for x in self.custom_column_label_map:
|
||||||
|
if self.custom_column_label_map[x]['is_multiple']:
|
||||||
|
SPLITABLE_FIELDS.append(MAP[x])
|
||||||
|
|
||||||
location = [location] if location != 'all' else list(MAP.keys())
|
location = [location] if location != 'all' else list(MAP.keys())
|
||||||
for i, loc in enumerate(location):
|
for i, loc in enumerate(location):
|
||||||
location[i] = MAP[loc]
|
location[i] = MAP[loc]
|
||||||
|
|
||||||
try:
|
try:
|
||||||
rating_query = int(query) * 2
|
rating_query = int(query) * 2
|
||||||
except:
|
except:
|
||||||
rating_query = None
|
rating_query = None
|
||||||
|
|
||||||
|
# get the tweak here so that the string lookup and compare aren't in the loop
|
||||||
|
bools_are_tristate = tweaks['bool_custom_columns_are_tristate'] == 'yes'
|
||||||
|
|
||||||
for loc in location:
|
for loc in location:
|
||||||
if loc == MAP['authors']:
|
if loc == MAP['authors']:
|
||||||
q = query.replace(',', '|'); ### DB stores authors with commas changed to bars, so change query
|
q = query.replace(',', '|'); ### DB stores authors with commas changed to bars, so change query
|
||||||
@ -278,14 +335,34 @@ class ResultCache(SearchQueryParser):
|
|||||||
|
|
||||||
for item in self._data:
|
for item in self._data:
|
||||||
if item is None: continue
|
if item is None: continue
|
||||||
|
|
||||||
|
if 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 not item[loc]:
|
||||||
if query == 'false':
|
if q == 'false':
|
||||||
if isinstance(item[loc], basestring):
|
|
||||||
if item[loc].strip() != '':
|
|
||||||
continue
|
|
||||||
matches.add(item[0])
|
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 q == 'true':
|
||||||
if isinstance(item[loc], basestring):
|
if isinstance(item[loc], basestring):
|
||||||
@ -293,12 +370,30 @@ class ResultCache(SearchQueryParser):
|
|||||||
continue
|
continue
|
||||||
matches.add(item[0])
|
matches.add(item[0])
|
||||||
continue
|
continue
|
||||||
if rating_query and loc == MAP['rating'] and rating_query == int(item[loc]):
|
|
||||||
matches.add(item[0])
|
if IS_CUSTOM[loc] == 'rating':
|
||||||
|
if rating_query and rating_query == int(item[loc]):
|
||||||
|
matches.add(item[0])
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
try: # a conversion below might fail
|
||||||
|
if IS_CUSTOM[loc] == 'float':
|
||||||
|
if float(query) == item[loc]: # relationals not supported
|
||||||
|
matches.add(item[0])
|
||||||
|
continue
|
||||||
|
if IS_CUSTOM[loc] == 'int':
|
||||||
|
if int(query) == item[loc]:
|
||||||
|
matches.add(item[0])
|
||||||
|
continue
|
||||||
|
except:
|
||||||
|
continue ## A conversion threw an exception. Because of the type, no further match possible
|
||||||
|
|
||||||
if loc not in EXCLUDE_FIELDS:
|
if loc not in EXCLUDE_FIELDS:
|
||||||
if loc in SPLITABLE_FIELDS:
|
if loc in SPLITABLE_FIELDS:
|
||||||
vals = item[loc].split(',') ### check individual tags/authors/formats, not the long string
|
if IS_CUSTOM[loc]:
|
||||||
|
vals = item[loc].split('|')
|
||||||
|
else:
|
||||||
|
vals = item[loc].split(',')
|
||||||
else:
|
else:
|
||||||
vals = [item[loc]] ### make into list to make _match happy
|
vals = [item[loc]] ### make into list to make _match happy
|
||||||
if _match(q, vals, matchkind):
|
if _match(q, vals, matchkind):
|
||||||
@ -342,8 +437,7 @@ class ResultCache(SearchQueryParser):
|
|||||||
'''
|
'''
|
||||||
for id in ids:
|
for id in ids:
|
||||||
try:
|
try:
|
||||||
self._data[id] = db.conn.get('SELECT * from meta2 WHERE id=?',
|
self._data[id] = db.conn.get('SELECT * from meta2 WHERE id=?', (id,))[0]
|
||||||
(id,))[0]
|
|
||||||
self._data[id].append(db.has_cover(id, index_is_id=True))
|
self._data[id].append(db.has_cover(id, index_is_id=True))
|
||||||
except IndexError:
|
except IndexError:
|
||||||
return None
|
return None
|
||||||
@ -399,6 +493,12 @@ class ResultCache(SearchQueryParser):
|
|||||||
asstr else cmp(self._data[x][loc], self._data[y][loc])
|
asstr else cmp(self._data[x][loc], self._data[y][loc])
|
||||||
except AttributeError: # Some entries may be None
|
except AttributeError: # Some entries may be None
|
||||||
ans = cmp(self._data[x][loc], self._data[y][loc])
|
ans = cmp(self._data[x][loc], self._data[y][loc])
|
||||||
|
except TypeError: ## raised when a datetime is None
|
||||||
|
if self._data[x][loc] is None:
|
||||||
|
if self._data[y][loc] is None:
|
||||||
|
return 0 # Both None. Return eq
|
||||||
|
return 1 # x is None, y not. Return gt
|
||||||
|
return -1 # x is not None and (therefore) y is. return lt
|
||||||
if subsort and ans == 0:
|
if subsort and ans == 0:
|
||||||
return cmp(self._data[x][11].lower(), self._data[y][11].lower())
|
return cmp(self._data[x][11].lower(), self._data[y][11].lower())
|
||||||
return ans
|
return ans
|
||||||
@ -410,21 +510,35 @@ class ResultCache(SearchQueryParser):
|
|||||||
if field == 'date': field = 'timestamp'
|
if field == 'date': field = 'timestamp'
|
||||||
elif field == 'title': field = 'sort'
|
elif field == 'title': field = 'sort'
|
||||||
elif field == 'authors': field = 'author_sort'
|
elif field == 'authors': field = 'author_sort'
|
||||||
|
as_string = field not in ('size', 'rating', 'timestamp')
|
||||||
|
if field in self.custom_column_label_map:
|
||||||
|
as_string = self.custom_column_label_map[field]['datatype'] in ('comments', 'text')
|
||||||
|
field = self.custom_column_label_map[field]['num']
|
||||||
|
|
||||||
if self.first_sort:
|
if self.first_sort:
|
||||||
subsort = True
|
subsort = True
|
||||||
self.first_sort = False
|
self.first_sort = False
|
||||||
fcmp = self.seriescmp if field == 'series' else \
|
fcmp = self.seriescmp if field == 'series' else \
|
||||||
functools.partial(self.cmp, self.FIELD_MAP[field], subsort=subsort,
|
functools.partial(self.cmp, self.FIELD_MAP[field], subsort=subsort,
|
||||||
asstr=field not in ('size', 'rating', 'timestamp'))
|
asstr=as_string)
|
||||||
|
|
||||||
self._map.sort(cmp=fcmp, reverse=not ascending)
|
self._map.sort(cmp=fcmp, reverse=not ascending)
|
||||||
self._map_filtered = [id for id in self._map if id in self._map_filtered]
|
self._map_filtered = [id for id in self._map if id in self._map_filtered]
|
||||||
|
|
||||||
def search(self, query):
|
def search(self, query, return_matches = False):
|
||||||
if not query or not query.strip():
|
if not query or not query.strip():
|
||||||
|
q = self.search_restriction
|
||||||
|
else:
|
||||||
|
q = '%s (%s)' % (self.search_restriction, query)
|
||||||
|
if not q:
|
||||||
|
if return_matches:
|
||||||
|
return list(self.map) # when return_matches, do not update the maps!
|
||||||
self._map_filtered = list(self._map)
|
self._map_filtered = list(self._map)
|
||||||
return
|
return []
|
||||||
matches = sorted(self.parse(query))
|
matches = sorted(self.parse(q))
|
||||||
|
if return_matches:
|
||||||
|
return [id for id in self._map if id in matches]
|
||||||
self._map_filtered = [id for id in self._map if id in matches]
|
self._map_filtered = [id for id in self._map if id in matches]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def set_search_restriction(self, s):
|
||||||
|
self.search_restriction = '' if not s else 'search:"%s"' % (s.strip())
|
@ -190,7 +190,7 @@ class CustomColumns(object):
|
|||||||
(label, num))
|
(label, num))
|
||||||
changed = True
|
changed = True
|
||||||
if is_editable is not None:
|
if is_editable is not None:
|
||||||
self.conn.execute('UPDATE custom_columns SET is_editable=? WHERE id=?',
|
self.conn.execute('UPDATE custom_columns SET editable=? WHERE id=?',
|
||||||
(bool(is_editable), num))
|
(bool(is_editable), num))
|
||||||
self.custom_column_num_map[num]['is_editable'] = bool(is_editable)
|
self.custom_column_num_map[num]['is_editable'] = bool(is_editable)
|
||||||
changed = True
|
changed = True
|
||||||
|
@ -56,16 +56,15 @@ def delete_tree(path, permanent=False):
|
|||||||
|
|
||||||
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Tag(object):
|
class Tag(object):
|
||||||
|
|
||||||
def __init__(self, name, id=None, count=0, state=0, tooltip=None):
|
def __init__(self, name, id=None, count=0, state=0, tooltip=None, icon=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.id = id
|
self.id = id
|
||||||
self.count = count
|
self.count = count
|
||||||
self.state = state
|
self.state = state
|
||||||
self.tooltip = tooltip
|
self.tooltip = tooltip
|
||||||
|
self.icon = icon
|
||||||
|
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return u'%s:%s:%s:%s:%s'%(self.name, self.count, self.id, self.state, self.tooltip)
|
return u'%s:%s:%s:%s:%s'%(self.name, self.count, self.id, self.state, self.tooltip)
|
||||||
@ -186,7 +185,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.conn.executescript(script)
|
self.conn.executescript(script)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
self.data = ResultCache(self.FIELD_MAP)
|
self.data = ResultCache(self.FIELD_MAP, self.custom_column_label_map)
|
||||||
self.search = self.data.search
|
self.search = self.data.search
|
||||||
self.refresh = functools.partial(self.data.refresh, self)
|
self.refresh = functools.partial(self.data.refresh, self)
|
||||||
self.sort = self.data.sort
|
self.sort = self.data.sort
|
||||||
@ -576,35 +575,99 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
def get_recipe(self, id):
|
def get_recipe(self, id):
|
||||||
return self.conn.get('SELECT script FROM feeds WHERE id=?', (id,), all=False)
|
return self.conn.get('SELECT script FROM feeds WHERE id=?', (id,), all=False)
|
||||||
|
|
||||||
def get_categories(self, sort_on_count=False):
|
def get_categories(self, sort_on_count=False, ids=None, icon_map=None):
|
||||||
self.conn.executescript(u'''
|
|
||||||
CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT
|
orig_category_columns = {'tags': ['tag', 'name'],
|
||||||
id,
|
'series': ['series', 'name'],
|
||||||
name,
|
'publishers': ['publisher', 'name'],
|
||||||
(SELECT COUNT(id) FROM books_tags_link WHERE tag=x.id) count
|
'authors': ['author', 'name']} # 'news' is added below
|
||||||
FROM tags as x WHERE name!="{0}" AND id IN
|
cat_cols = {}
|
||||||
(SELECT DISTINCT tag FROM books_tags_link WHERE book IN
|
|
||||||
(SELECT DISTINCT book FROM books_tags_link WHERE tag IN
|
def create_filtered_views(self, ids):
|
||||||
(SELECT id FROM tags WHERE name="{0}")));
|
def create_tag_browser_view(table_name, column_name, view_column_name):
|
||||||
'''.format(_('News')))
|
script = ('''
|
||||||
self.conn.commit()
|
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 = {}
|
categories = {}
|
||||||
for x in ('tags', 'series', 'news', 'publishers', 'authors'):
|
for tn,cn in cat_cols.iteritems():
|
||||||
query = 'SELECT id,name,count FROM tag_browser_'+x
|
if ids is None:
|
||||||
|
query = 'SELECT id, {0}, count FROM tag_browser_{1}'.format(cn[1], tn)
|
||||||
|
else:
|
||||||
|
query = 'SELECT id, {0}, count FROM tag_browser_filtered_{1}'.format(cn[1], tn)
|
||||||
if sort_on_count:
|
if sort_on_count:
|
||||||
query += ' ORDER BY count DESC'
|
query += ' ORDER BY count DESC'
|
||||||
else:
|
else:
|
||||||
query += ' ORDER BY name ASC'
|
query += ' ORDER BY {0} ASC'.format(cn[1])
|
||||||
data = self.conn.get(query)
|
data = self.conn.get(query)
|
||||||
category = x if x in ('series', 'news') else x[:-1]
|
category = cn[0]
|
||||||
categories[category] = [Tag(r[1], count=r[2], id=r[0]) for r in data]
|
icon = icon_map[category] if category in icon_map else icon_map['*custom']
|
||||||
|
if ids is None: # no filtering
|
||||||
|
categories[category] = [Tag(r[1], count=r[2], id=r[0], icon=icon)
|
||||||
|
for r in data]
|
||||||
|
else: # filter out zero-count tags
|
||||||
|
categories[category] = [Tag(r[1], count=r[2], id=r[0], icon=icon)
|
||||||
|
for r in data if r[2] > 0]
|
||||||
categories['format'] = []
|
categories['format'] = []
|
||||||
for fmt in self.conn.get('SELECT DISTINCT format FROM data'):
|
for fmt in self.conn.get('SELECT DISTINCT format FROM data'):
|
||||||
fmt = fmt[0]
|
fmt = fmt[0]
|
||||||
count = self.conn.get('SELECT COUNT(id) FROM data WHERE format="%s"'%fmt,
|
if ids is not None:
|
||||||
all=False)
|
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))
|
categories['format'].append(Tag(fmt, count=count))
|
||||||
|
|
||||||
if sort_on_count:
|
if sort_on_count:
|
||||||
@ -612,7 +675,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
reverse=True)
|
reverse=True)
|
||||||
else:
|
else:
|
||||||
categories['format'].sort(cmp=lambda x,y:cmp(x.name, y.name))
|
categories['format'].sort(cmp=lambda x,y:cmp(x.name, y.name))
|
||||||
|
|
||||||
return categories
|
return categories
|
||||||
|
|
||||||
def tags_older_than(self, tag, delta):
|
def tags_older_than(self, tag, delta):
|
||||||
|
@ -24,6 +24,13 @@ class SafeLocalTimeZone(tzlocal):
|
|||||||
pass
|
pass
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def compute_locale_info_for_parse_date():
|
||||||
|
dt = datetime.strptime('1/5/2000', "%x")
|
||||||
|
if dt.month == 5:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
parse_date_day_first = compute_locale_info_for_parse_date()
|
||||||
utc_tz = _utc_tz = tzutc()
|
utc_tz = _utc_tz = tzutc()
|
||||||
local_tz = _local_tz = SafeLocalTimeZone()
|
local_tz = _local_tz = SafeLocalTimeZone()
|
||||||
|
|
||||||
@ -44,7 +51,7 @@ def parse_date(date_string, assume_utc=False, as_utc=True, default=None):
|
|||||||
func = datetime.utcnow if assume_utc else datetime.now
|
func = datetime.utcnow if assume_utc else datetime.now
|
||||||
default = func().replace(hour=0, minute=0, second=0, microsecond=0,
|
default = func().replace(hour=0, minute=0, second=0, microsecond=0,
|
||||||
tzinfo=_utc_tz if assume_utc else _local_tz)
|
tzinfo=_utc_tz if assume_utc else _local_tz)
|
||||||
dt = parse(date_string, default=default)
|
dt = parse(date_string, default=default, dayfirst=parse_date_day_first)
|
||||||
if dt.tzinfo is None:
|
if dt.tzinfo is None:
|
||||||
dt = dt.replace(tzinfo=_utc_tz if assume_utc else _local_tz)
|
dt = dt.replace(tzinfo=_utc_tz if assume_utc else _local_tz)
|
||||||
return dt.astimezone(_utc_tz if as_utc else _local_tz)
|
return dt.astimezone(_utc_tz if as_utc else _local_tz)
|
||||||
|
@ -116,13 +116,12 @@ class SearchQueryParser(object):
|
|||||||
failed.append(test[0])
|
failed.append(test[0])
|
||||||
return failed
|
return failed
|
||||||
|
|
||||||
def __init__(self, test=False):
|
def __init__(self, custcols=[], test=False):
|
||||||
self._tests_failed = False
|
self._tests_failed = False
|
||||||
# Define a token
|
# Define a token
|
||||||
locations = map(lambda x : CaselessLiteral(x)+Suppress(':'),
|
standard_locations = map(lambda x : CaselessLiteral(x)+Suppress(':'), self.LOCATIONS+custcols)
|
||||||
self.LOCATIONS)
|
|
||||||
location = NoMatch()
|
location = NoMatch()
|
||||||
for l in locations:
|
for l in standard_locations:
|
||||||
location |= l
|
location |= l
|
||||||
location = Optional(location, default='all')
|
location = Optional(location, default='all')
|
||||||
word_query = CharsNotIn(string.whitespace + '()')
|
word_query = CharsNotIn(string.whitespace + '()')
|
||||||
@ -176,14 +175,20 @@ class SearchQueryParser(object):
|
|||||||
|
|
||||||
def parse(self, query):
|
def parse(self, query):
|
||||||
# empty the list of searches used for recursion testing
|
# empty the list of searches used for recursion testing
|
||||||
|
self.recurse_level = 0
|
||||||
self.searches_seen = set([])
|
self.searches_seen = set([])
|
||||||
return self._parse(query)
|
return self._parse(query)
|
||||||
|
|
||||||
# this parse is used internally because it doesn't clear the
|
# this parse is used internally because it doesn't clear the
|
||||||
# recursive search test list
|
# recursive search test list. However, we permit seeing the
|
||||||
|
# same search a few times because the search might appear within
|
||||||
|
# another search.
|
||||||
def _parse(self, query):
|
def _parse(self, query):
|
||||||
|
self.recurse_level += 1
|
||||||
res = self._parser.parseString(query)[0]
|
res = self._parser.parseString(query)[0]
|
||||||
return self.evaluate(res)
|
t = self.evaluate(res)
|
||||||
|
self.recurse_level -= 1
|
||||||
|
return t
|
||||||
|
|
||||||
def method(self, group_name):
|
def method(self, group_name):
|
||||||
return getattr(self, 'evaluate_'+group_name)
|
return getattr(self, 'evaluate_'+group_name)
|
||||||
@ -207,13 +212,13 @@ class SearchQueryParser(object):
|
|||||||
location = argument[0]
|
location = argument[0]
|
||||||
query = argument[1]
|
query = argument[1]
|
||||||
if location.lower() == 'search':
|
if location.lower() == 'search':
|
||||||
# print "looking for named search " + query
|
|
||||||
if query.startswith('='):
|
if query.startswith('='):
|
||||||
query = query[1:]
|
query = query[1:]
|
||||||
try:
|
try:
|
||||||
if query in self.searches_seen:
|
if query in self.searches_seen:
|
||||||
raise ParseException(query, len(query), 'undefined saved search', self)
|
raise ParseException(query, len(query), 'undefined saved search', self)
|
||||||
self.searches_seen.add(query)
|
if self.recurse_level > 5:
|
||||||
|
self.searches_seen.add(query)
|
||||||
return self._parse(saved_searches.lookup(query))
|
return self._parse(saved_searches.lookup(query))
|
||||||
except: # convert all exceptions (e.g., missing key) to a parse error
|
except: # convert all exceptions (e.g., missing key) to a parse error
|
||||||
raise ParseException(query, len(query), 'undefined saved search', self)
|
raise ParseException(query, len(query), 'undefined saved search', self)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user