From a1afc8b7eb5dcb7f65d82e6e7252bb3b070afa2e Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 1 Jun 2011 14:23:09 -0600
Subject: [PATCH 01/14] ...
---
src/calibre/gui2/library/coloring.py | 12 +++++++-----
1 file changed, 7 insertions(+), 5 deletions(-)
diff --git a/src/calibre/gui2/library/coloring.py b/src/calibre/gui2/library/coloring.py
index 9dd284481b..7fb45d9ba6 100644
--- a/src/calibre/gui2/library/coloring.py
+++ b/src/calibre/gui2/library/coloring.py
@@ -66,7 +66,7 @@ class Rule(object): # {{{
return dedent('''\
program:
{sig}
- test(and('1',
+ test(and(
{conditions}
), {color}, '');
''').format(sig=self.signature, conditions=conditions,
@@ -113,7 +113,7 @@ class Rule(object): # {{{
'lt': ('1', '', ''),
'gt': ('', '', '1')
}[action]
- return "cmp(format_date('%s', 'yyyy-MM-dd'), %s, '%s', '%s', '%s')" % (col,
+ return "cmp(format_date(raw_field('%s'), 'yyyy-MM-dd'), %s, '%s', '%s', '%s')" % (col,
val, lt, eq, gt)
def multiple_condition(self, col, action, val, sep):
@@ -246,7 +246,7 @@ class ConditionEditor(QWidget):
for key in sorted(
conditionable_columns(fm),
key=lambda x:sort_key(fm[x]['name'])):
- self.column_box.addItem(fm[key]['name'], key)
+ self.column_box.addItem(key, key)
self.column_box.setCurrentIndex(0)
self.column_box.currentIndexChanged.connect(self.init_action_box)
@@ -352,7 +352,8 @@ class ConditionEditor(QWidget):
if 'pattern' in action:
tt = _('Enter a regular expression')
self.value_box.setToolTip(tt)
- if action in ('is set', 'is not set'):
+ if action in ('is set', 'is not set', 'is true', 'is false',
+ 'is undefined'):
self.value_box.setEnabled(False)
@@ -418,6 +419,7 @@ class RuleEditor(QDialog):
self.conditions_widget = QWidget(self)
sa.setWidget(self.conditions_widget)
self.conditions_widget.setLayout(QVBoxLayout())
+ self.conditions_widget.layout().setAlignment(Qt.AlignTop)
self.conditions = []
for b in (self.column_box, self.color_box):
@@ -429,7 +431,7 @@ class RuleEditor(QDialog):
key=lambda x:sort_key(fm[x]['name'])):
name = fm[key]['name']
if name:
- self.column_box.addItem(name, key)
+ self.column_box.addItem(key, key)
self.column_box.setCurrentIndex(0)
self.color_box.addItems(QColor.colorNames())
From 739693060d2373cccf0f091fa7100842b3cb2ab2 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 1 Jun 2011 15:18:49 -0600
Subject: [PATCH 02/14] Autogen the template function docs
---
src/calibre/manual/custom.py | 12 +-
src/calibre/manual/template_ref.rst | 266 --------------------
src/calibre/manual/template_ref_generate.py | 5 +-
3 files changed, 13 insertions(+), 270 deletions(-)
delete mode 100644 src/calibre/manual/template_ref.rst
diff --git a/src/calibre/manual/custom.py b/src/calibre/manual/custom.py
index f5db6dd0c2..4788889972 100644
--- a/src/calibre/manual/custom.py
+++ b/src/calibre/manual/custom.py
@@ -240,11 +240,21 @@ def cli_docs(app):
raw += '\n'+'\n'.join(lines)
update_cli_doc(os.path.join('cli', cmd+'.rst'), raw, info)
+def generate_docs(app):
+ cli_docs(app)
+ template_docs(app)
+
+def template_docs(app):
+ from template_ref_generate import generate_template_language_help
+ info = app.builder.info
+ raw = generate_template_language_help()
+ update_cli_doc('template_ref.rst', raw, info)
+
def setup(app):
app.add_config_value('epub_cover', None, False)
app.add_builder(EPUBHelpBuilder)
app.connect('doctree-read', substitute)
- app.connect('builder-inited', cli_docs)
+ app.connect('builder-inited', generate_docs)
app.connect('build-finished', finished)
def finished(app, exception):
diff --git a/src/calibre/manual/template_ref.rst b/src/calibre/manual/template_ref.rst
deleted file mode 100644
index de6c1fdb2c..0000000000
--- a/src/calibre/manual/template_ref.rst
+++ /dev/null
@@ -1,266 +0,0 @@
-.. include:: global.rst
-
-.. _templaterefcalibre:
-
-Reference for all builtin template language functions
-========================================================
-
-Here, we document all the builtin functions available in the |app| template language. Every function is implemented as a class in python and you can click the source links to see the source code, in case the documentation is insufficient. The functions are arranged in logical groups by type.
-
-.. contents::
- :depth: 2
- :local:
-
-.. module:: calibre.utils.formatter_functions
-
-Get values from metadata
---------------------------
-
-field(name)
-^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinField
-
-raw_field(name)
-^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinRaw_field
-
-booksize()
-^^^^^^^^^^^^
-
-.. autoclass:: BuiltinBooksize
-
-format_date(val, format_string)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinFormatDate
-
-ondevice()
-^^^^^^^^^^^
-
-.. autoclass:: BuiltinOndevice
-
-Arithmetic
--------------
-
-add(x, y)
-^^^^^^^^^^^^^
-.. autoclass:: BuiltinAdd
-
-subtract(x, y)
-^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinSubtract
-
-multiply(x, y)
-^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinMultiply
-
-divide(x, y)
-^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinDivide
-
-Boolean
-------------
-
-and(value1, value2, ...)
-^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinAnd
-
-or(value1, value2, ...)
-^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinOr
-
-not(value)
-^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinNot
-
-If-then-else
------------------
-
-contains(val, pattern, text if match, text if not match)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinContains
-
-test(val, text if not empty, text if empty)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinTest
-
-ifempty(val, text if empty)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinIfempty
-
-Iterating over values
-------------------------
-
-first_non_empty(value, value, ...)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinFirstNonEmpty
-
-lookup(val, pattern, field, pattern, field, ..., else_field)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinLookup
-
-switch(val, pattern, value, pattern, value, ..., else_value)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinSwitch
-
-List Lookup
----------------
-
-in_list(val, separator, pattern, found_val, not_found_val)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinInList
-
-str_in_list(val, separator, string, found_val, not_found_val)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinStrInList
-
-list_item(val, index, separator)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinListitem
-
-select(val, key)
-^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinSelect
-
-
-List Manipulation
--------------------
-
-count(val, separator)
-^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinCount
-
-merge_lists(list1, list2, separator)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinMergeLists
-
-sublist(val, start_index, end_index, separator)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinSublist
-
-subitems(val, start_index, end_index)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinSubitems
-
-Recursion
--------------
-
-eval(template)
-^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinEval
-
-template(x)
-^^^^^^^^^^^^
-
-.. autoclass:: BuiltinTemplate
-
-Relational
------------
-
-cmp(x, y, lt, eq, gt)
-^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinCmp
-
-strcmp(x, y, lt, eq, gt)
-^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinStrcmp
-
-String case changes
----------------------
-
-lowercase(val)
-^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinLowercase
-
-uppercase(val)
-^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinUppercase
-
-titlecase(val)
-^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinTitlecase
-
-capitalize(val)
-^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinCapitalize
-
-String Manipulation
----------------------
-
-re(val, pattern, replacement)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinRe
-
-shorten(val, left chars, middle text, right chars)
-^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinShorten
-
-substr(str, start, end)
-^^^^^^^^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinSubstr
-
-
-Other
---------
-
-assign(id, val)
-^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinAssign
-
-print(a, b, ...)
-^^^^^^^^^^^^^^^^^
-
-.. autoclass:: BuiltinPrint
-
-
-API of the Metadata objects
-----------------------------
-
-The python implementation of the template functions is passed in a Metadata object. Knowing it's API is useful if you want to define your own template functions.
-
-.. module:: calibre.ebooks.metadata.book.base
-
-.. autoclass:: Metadata
- :members:
- :member-order: bysource
-
-.. data:: STANDARD_METADATA_FIELDS
-
- The set of standard metadata fields.
-
-.. literalinclude:: ../ebooks/metadata/book/__init__.py
- :lines: 7-
-
diff --git a/src/calibre/manual/template_ref_generate.py b/src/calibre/manual/template_ref_generate.py
index 8618eb9f07..742ab1fd54 100644
--- a/src/calibre/manual/template_ref_generate.py
+++ b/src/calibre/manual/template_ref_generate.py
@@ -86,8 +86,7 @@ def generate_template_language_help():
hats='^'*len(entry))
output += POSTAMBLE
- print output
- return output # and hope that something good happens to it
+ return output
if __name__ == '__main__':
- generate_template_language_help()
\ No newline at end of file
+ generate_template_language_help()
From cff2fcb6eb1caf0529159a65aa68d78aec5dd9da Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 1 Jun 2011 15:19:24 -0600
Subject: [PATCH 03/14] ...
---
.bzrignore | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/.bzrignore b/.bzrignore
index 005391bf46..d2a2d592dd 100644
--- a/.bzrignore
+++ b/.bzrignore
@@ -4,6 +4,7 @@ src/calibre/plugins
resources/images.qrc
src/calibre/manual/.build/
src/calibre/manual/cli/
+src/calibre/manual/template_ref.rst
build
dist
docs
@@ -31,4 +32,4 @@ nbproject/
.pydevproject
.settings/
*.DS_Store
-calibre_plugins/
\ No newline at end of file
+calibre_plugins/
From 04c5fcc2eee8152840f723137497156dfa11b98b Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 1 Jun 2011 15:24:49 -0600
Subject: [PATCH 04/14] Split non-GUI part of the coloring code into a non GUI
module
---
src/calibre/gui2/library/coloring.py | 171 +------------------------
src/calibre/library/coloring.py | 178 +++++++++++++++++++++++++++
2 files changed, 180 insertions(+), 169 deletions(-)
create mode 100644 src/calibre/library/coloring.py
diff --git a/src/calibre/gui2/library/coloring.py b/src/calibre/gui2/library/coloring.py
index 7fb45d9ba6..4c8d678b19 100644
--- a/src/calibre/gui2/library/coloring.py
+++ b/src/calibre/gui2/library/coloring.py
@@ -2,186 +2,19 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
-from future_builtins import map
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-import json, binascii, re
-from textwrap import dedent
-
from PyQt4.Qt import (QWidget, QDialog, QLabel, QGridLayout, QComboBox,
QLineEdit, QIntValidator, QDoubleValidator, QFrame, QColor, Qt, QIcon,
QScrollArea, QPushButton, QVBoxLayout, QDialogButtonBox)
from calibre.utils.icu import sort_key
from calibre.gui2 import error_dialog
-
-class Rule(object): # {{{
-
- SIGNATURE = '# BasicColorRule():'
-
- def __init__(self, fm):
- self.color = None
- self.fm = fm
- self.conditions = []
-
- def add_condition(self, col, action, val):
- if col not in self.fm:
- raise ValueError('%r is not a valid column name'%col)
- v = self.validate_condition(col, action, val)
- if v:
- raise ValueError(v)
- self.conditions.append((col, action, val))
-
- def validate_condition(self, col, action, val):
- m = self.fm[col]
- dt = m['datatype']
- if (dt in ('int', 'float', 'rating') and action in ('lt', 'eq', 'gt')):
- try:
- int(val) if dt == 'int' else float(val)
- except:
- return '%r is not a valid numerical value'%val
-
- if (dt in ('comments', 'series', 'text', 'enumeration') and 'pattern'
- in action):
- try:
- re.compile(val)
- except:
- return '%r is not a valid regular expression'%val
-
- @property
- def signature(self):
- args = (self.color, self.conditions)
- sig = json.dumps(args, ensure_ascii=False)
- return self.SIGNATURE + binascii.hexlify(sig.encode('utf-8'))
-
- @property
- def template(self):
- if not self.color or not self.conditions:
- return None
- conditions = map(self.apply_condition, self.conditions)
- conditions = (',\n' + ' '*9).join(conditions)
- return dedent('''\
- program:
- {sig}
- test(and(
- {conditions}
- ), {color}, '');
- ''').format(sig=self.signature, conditions=conditions,
- color=self.color)
-
- def apply_condition(self, condition):
- col, action, val = condition
- m = self.fm[col]
- dt = m['datatype']
-
- if dt == 'bool':
- return self.bool_condition(col, action, val)
-
- if dt in ('int', 'float', 'rating'):
- return self.number_condition(col, action, val)
-
- if dt == 'datetime':
- return self.date_condition(col, action, val)
-
- if dt in ('comments', 'series', 'text', 'enumeration'):
- ism = m.get('is_multiple', False)
- if ism:
- return self.multiple_condition(col, action, val, ism)
- return self.text_condition(col, action, val)
-
- def bool_condition(self, col, action, val):
- test = {'is true': 'True',
- 'is false': 'False',
- 'is undefined': 'None'}[action]
- return "strcmp('%s', raw_field('%s'), '', '1', '')"%(test, col)
-
- def number_condition(self, col, action, val):
- lt, eq, gt = {
- 'eq': ('', '1', ''),
- 'lt': ('1', '', ''),
- 'gt': ('', '', '1')
- }[action]
- lt, eq, gt = '', '1', ''
- return "cmp(field('%s'), %s, '%s', '%s', '%s')" % (col, val, lt, eq, gt)
-
- def date_condition(self, col, action, val):
- lt, eq, gt = {
- 'eq': ('', '1', ''),
- 'lt': ('1', '', ''),
- 'gt': ('', '', '1')
- }[action]
- return "cmp(format_date(raw_field('%s'), 'yyyy-MM-dd'), %s, '%s', '%s', '%s')" % (col,
- val, lt, eq, gt)
-
- def multiple_condition(self, col, action, val, sep):
- if action == 'is set':
- return "test('%s', '1', '')"%col
- if action == 'is not set':
- return "test('%s', '', '1')"%col
- if action == 'has':
- return "str_in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val)
- if action == 'does not have':
- return "str_in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val)
- if action == 'has pattern':
- return "in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val)
- if action == 'does not have pattern':
- return "in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val)
-
- def text_condition(self, col, action, val):
- if action == 'is set':
- return "test('%s', '1', '')"%col
- if action == 'is not set':
- return "test('%s', '', '1')"%col
- if action == 'is':
- return "strcmp(field('%s'), \"%s\", '', '1', '')"%(col, val)
- if action == 'is not':
- return "strcmp(field('%s'), \"%s\", '1', '', '1')"%(col, val)
- if action == 'matches pattern':
- return "contains(field('%s'), \"%s\", '1', '')"%(col, val)
- if action == 'does not match pattern':
- return "contains(field('%s'), \"%s\", '', '1')"%(col, val)
-
-# }}}
-
-def rule_from_template(fm, template):
- ok_lines = []
- for line in template.splitlines():
- if line.startswith(Rule.SIGNATURE):
- raw = line[len(Rule.SIGNATURE):].strip()
- try:
- color, conditions = json.loads(binascii.unhexlify(raw).decode('utf-8'))
- except:
- continue
- r = Rule(fm)
- r.color = color
- for c in conditions:
- try:
- r.add_condition(*c)
- except:
- continue
- if r.color and r.conditions:
- return r
- else:
- ok_lines.append(line)
- return '\n'.join(ok_lines)
-
-def conditionable_columns(fm):
- for key in fm:
- m = fm[key]
- dt = m['datatype']
- if m.get('name', False) and dt in ('bool', 'int', 'float', 'rating', 'series',
- 'comments', 'text', 'enumeration', 'datetime'):
- yield key
-
-
-def displayable_columns(fm):
- for key in fm.displayable_field_keys():
- if key not in ('sort', 'author_sort', 'comments', 'formats',
- 'identifiers', 'path'):
- yield key
+from calibre.library.coloring import (Rule, conditionable_columns,
+ displayable_columns)
class ConditionEditor(QWidget):
diff --git a/src/calibre/library/coloring.py b/src/calibre/library/coloring.py
new file mode 100644
index 0000000000..7e2b0f67c6
--- /dev/null
+++ b/src/calibre/library/coloring.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+from __future__ import (unicode_literals, division, absolute_import,
+ print_function)
+from future_builtins import map
+
+__license__ = 'GPL v3'
+__copyright__ = '2011, Kovid Goyal '
+__docformat__ = 'restructuredtext en'
+
+import binascii, re, json
+from textwrap import dedent
+
+class Rule(object): # {{{
+
+ SIGNATURE = '# BasicColorRule():'
+
+ def __init__(self, fm, color=None):
+ self.color = color
+ self.fm = fm
+ self.conditions = []
+
+ def add_condition(self, col, action, val):
+ if col not in self.fm:
+ raise ValueError('%r is not a valid column name'%col)
+ v = self.validate_condition(col, action, val)
+ if v:
+ raise ValueError(v)
+ self.conditions.append((col, action, val))
+
+ def validate_condition(self, col, action, val):
+ m = self.fm[col]
+ dt = m['datatype']
+ if (dt in ('int', 'float', 'rating') and action in ('lt', 'eq', 'gt')):
+ try:
+ int(val) if dt == 'int' else float(val)
+ except:
+ return '%r is not a valid numerical value'%val
+
+ if (dt in ('comments', 'series', 'text', 'enumeration') and 'pattern'
+ in action):
+ try:
+ re.compile(val)
+ except:
+ return '%r is not a valid regular expression'%val
+
+ @property
+ def signature(self):
+ args = (self.color, self.conditions)
+ sig = json.dumps(args, ensure_ascii=False)
+ return self.SIGNATURE + binascii.hexlify(sig.encode('utf-8'))
+
+ @property
+ def template(self):
+ if not self.color or not self.conditions:
+ return None
+ conditions = map(self.apply_condition, self.conditions)
+ conditions = (',\n' + ' '*9).join(conditions)
+ return dedent('''\
+ program:
+ {sig}
+ test(and(
+ {conditions}
+ ), {color}, '');
+ ''').format(sig=self.signature, conditions=conditions,
+ color=self.color)
+
+ def apply_condition(self, condition):
+ col, action, val = condition
+ m = self.fm[col]
+ dt = m['datatype']
+
+ if dt == 'bool':
+ return self.bool_condition(col, action, val)
+
+ if dt in ('int', 'float', 'rating'):
+ return self.number_condition(col, action, val)
+
+ if dt == 'datetime':
+ return self.date_condition(col, action, val)
+
+ if dt in ('comments', 'series', 'text', 'enumeration'):
+ ism = m.get('is_multiple', False)
+ if ism:
+ return self.multiple_condition(col, action, val, ism)
+ return self.text_condition(col, action, val)
+
+ def bool_condition(self, col, action, val):
+ test = {'is true': 'True',
+ 'is false': 'False',
+ 'is undefined': 'None'}[action]
+ return "strcmp('%s', raw_field('%s'), '', '1', '')"%(test, col)
+
+ def number_condition(self, col, action, val):
+ lt, eq, gt = {
+ 'eq': ('', '1', ''),
+ 'lt': ('1', '', ''),
+ 'gt': ('', '', '1')
+ }[action]
+ lt, eq, gt = '', '1', ''
+ return "cmp(field('%s'), %s, '%s', '%s', '%s')" % (col, val, lt, eq, gt)
+
+ def date_condition(self, col, action, val):
+ lt, eq, gt = {
+ 'eq': ('', '1', ''),
+ 'lt': ('1', '', ''),
+ 'gt': ('', '', '1')
+ }[action]
+ return "cmp(format_date(raw_field('%s'), 'yyyy-MM-dd'), %s, '%s', '%s', '%s')" % (col,
+ val, lt, eq, gt)
+
+ def multiple_condition(self, col, action, val, sep):
+ if action == 'is set':
+ return "test('%s', '1', '')"%col
+ if action == 'is not set':
+ return "test('%s', '', '1')"%col
+ if action == 'has':
+ return "str_in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val)
+ if action == 'does not have':
+ return "str_in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val)
+ if action == 'has pattern':
+ return "in_list(field('%s'), '%s', \"%s\", '1', '')"%(col, sep, val)
+ if action == 'does not have pattern':
+ return "in_list(field('%s'), '%s', \"%s\", '', '1')"%(col, sep, val)
+
+ def text_condition(self, col, action, val):
+ if action == 'is set':
+ return "test('%s', '1', '')"%col
+ if action == 'is not set':
+ return "test('%s', '', '1')"%col
+ if action == 'is':
+ return "strcmp(field('%s'), \"%s\", '', '1', '')"%(col, val)
+ if action == 'is not':
+ return "strcmp(field('%s'), \"%s\", '1', '', '1')"%(col, val)
+ if action == 'matches pattern':
+ return "contains(field('%s'), \"%s\", '1', '')"%(col, val)
+ if action == 'does not match pattern':
+ return "contains(field('%s'), \"%s\", '', '1')"%(col, val)
+
+# }}}
+
+def rule_from_template(fm, template):
+ ok_lines = []
+ for line in template.splitlines():
+ if line.startswith(Rule.SIGNATURE):
+ raw = line[len(Rule.SIGNATURE):].strip()
+ try:
+ color, conditions = json.loads(binascii.unhexlify(raw).decode('utf-8'))
+ except:
+ continue
+ r = Rule(fm)
+ r.color = color
+ for c in conditions:
+ try:
+ r.add_condition(*c)
+ except:
+ continue
+ if r.color and r.conditions:
+ return r
+ else:
+ ok_lines.append(line)
+ return '\n'.join(ok_lines)
+
+def conditionable_columns(fm):
+ for key in fm:
+ m = fm[key]
+ dt = m['datatype']
+ if m.get('name', False) and dt in ('bool', 'int', 'float', 'rating', 'series',
+ 'comments', 'text', 'enumeration', 'datetime'):
+ yield key
+
+
+def displayable_columns(fm):
+ for key in fm.displayable_field_keys():
+ if key not in ('sort', 'author_sort', 'comments', 'formats',
+ 'identifiers', 'path'):
+ yield key
+
From fcc6ec5de59a7424eaf685c2c709530b4ec5b7c2 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 1 Jun 2011 15:28:54 -0600
Subject: [PATCH 05/14] ...
---
src/calibre/gui2/library/coloring.py | 11 ++++++++---
1 file changed, 8 insertions(+), 3 deletions(-)
diff --git a/src/calibre/gui2/library/coloring.py b/src/calibre/gui2/library/coloring.py
index 4c8d678b19..98ac7380a5 100644
--- a/src/calibre/gui2/library/coloring.py
+++ b/src/calibre/gui2/library/coloring.py
@@ -16,7 +16,7 @@ from calibre.gui2 import error_dialog
from calibre.library.coloring import (Rule, conditionable_columns,
displayable_columns)
-class ConditionEditor(QWidget):
+class ConditionEditor(QWidget): # {{{
def __init__(self, fm, parent=None):
QWidget.__init__(self, parent)
@@ -188,9 +188,9 @@ class ConditionEditor(QWidget):
if action in ('is set', 'is not set', 'is true', 'is false',
'is undefined'):
self.value_box.setEnabled(False)
+# }}}
-
-class RuleEditor(QDialog):
+class RuleEditor(QDialog): # {{{
def __init__(self, fm, parent=None):
QDialog.__init__(self, parent)
@@ -314,8 +314,13 @@ class RuleEditor(QDialog):
r.add_condition(*condition)
return col, r
+# }}}
+class EditRules(QWidget):
+ def __init__(self, db, parent=None):
+ QWidget.__init__(self, parent)
+ self.db = db
if __name__ == '__main__':
from PyQt4.Qt import QApplication
From d559f7df719643ac46101ed4ce157ac0549ceae9 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 1 Jun 2011 15:29:31 -0600
Subject: [PATCH 06/14] ...
---
src/calibre/gui2/{library => preferences}/coloring.py | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename src/calibre/gui2/{library => preferences}/coloring.py (100%)
diff --git a/src/calibre/gui2/library/coloring.py b/src/calibre/gui2/preferences/coloring.py
similarity index 100%
rename from src/calibre/gui2/library/coloring.py
rename to src/calibre/gui2/preferences/coloring.py
From fc8f268ee9ce4ce77f1c4050f2da554b0a226e03 Mon Sep 17 00:00:00 2001
From: John Schember
Date: Wed, 1 Jun 2011 18:22:55 -0400
Subject: [PATCH 07/14] Store: EpubBud store.
---
src/calibre/customize/builtins.py | 10 +++
src/calibre/gui2/store/epubbud_plugin.py | 81 ++++++++++++++++++++++++
2 files changed, 91 insertions(+)
create mode 100644 src/calibre/gui2/store/epubbud_plugin.py
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index 5cde30f72e..9ebec5e7e8 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -1227,6 +1227,15 @@ class StoreEHarlequinStore(StoreBase):
formats = ['EPUB', 'PDF']
affiliate = True
+class StoreEpubBudStore(StoreBase):
+ name = 'ePub Bud'
+ description = 'Well, it\'s pretty much just "YouTube for Children\'s eBooks. A not-for-profit organization devoted to brining self published childrens books to the world.'
+ actual_plugin = 'calibre.gui2.store.epubbud_plugin:EpubBudStore'
+
+ drm_free_only = True
+ headquarters = 'US'
+ formats = ['EPUB']
+
class StoreFeedbooksStore(StoreBase):
name = 'Feedbooks'
description = u'Feedbooks is a cloud publishing and distribution service, connected to a large ecosystem of reading systems and social networks. Provides a variety of genres from independent and classic books.'
@@ -1422,6 +1431,7 @@ plugins += [
StoreEBookShoppeUKStore,
StoreEPubBuyDEStore,
StoreEHarlequinStore,
+ StoreEpubBudStore,
StoreFeedbooksStore,
StoreFoylesUKStore,
StoreGandalfStore,
diff --git a/src/calibre/gui2/store/epubbud_plugin.py b/src/calibre/gui2/store/epubbud_plugin.py
new file mode 100644
index 0000000000..6c20f5150d
--- /dev/null
+++ b/src/calibre/gui2/store/epubbud_plugin.py
@@ -0,0 +1,81 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import (unicode_literals, division, absolute_import, print_function)
+
+__license__ = 'GPL 3'
+__copyright__ = '2011, John Schember '
+__docformat__ = 'restructuredtext en'
+
+import urllib
+from contextlib import closing
+
+from lxml import html
+
+from PyQt4.Qt import QUrl
+
+from calibre import browser, url_slash_cleaner
+from calibre.gui2 import open_url
+from calibre.gui2.store import StorePlugin
+from calibre.gui2.store.basic_config import BasicStoreConfig
+from calibre.gui2.store.search_result import SearchResult
+from calibre.gui2.store.web_store_dialog import WebStoreDialog
+
+class EpubBudStore(BasicStoreConfig, StorePlugin):
+
+ def open(self, parent=None, detail_item=None, external=False):
+ url = 'http://epubbud.com/'
+
+ if detail_item:
+ url = 'http://epubbud.com/book.php?g=' + detail_item
+
+ if external or self.config.get('open_external', False):
+ open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
+ else:
+ d = WebStoreDialog(self.gui, url, parent, detail_item)
+ d.setWindowTitle(self.name)
+ d.set_tags(self.config.get('tags', ''))
+ d.exec_()
+
+ def search(self, query, max_results=10, timeout=60):
+ '''
+ OPDS based search.
+
+ We really should get the catelog from http://pragprog.com/catalog.opds
+ and look for the application/opensearchdescription+xml entry.
+ Then get the opensearch description to get the search url and
+ format. However, we are going to be lazy and hard code it.
+ '''
+ url = 'http://www.epubbud.com/search.php?format=atom&q=' + urllib.quote_plus(query)
+
+ br = browser()
+
+ counter = max_results
+ with closing(br.open(url, timeout=timeout)) as f:
+ # Use html instead of etree as html allows us
+ # to ignore the namespace easily.
+ doc = html.fromstring(f.read())
+ for data in doc.xpath('//entry'):
+ if counter <= 0:
+ break
+
+ id = ''.join(data.xpath('.//id/text()'))
+ if not id:
+ continue
+
+ cover_url = ''.join(data.xpath('.//link[@rel="http://opds-spec.org/thumbnail"]/@href'))
+
+ title = u''.join(data.xpath('.//title/text()'))
+ author = u''.join(data.xpath('.//author/name/text()'))
+
+ counter -= 1
+
+ s = SearchResult()
+ s.cover_url = cover_url
+ s.title = title.strip()
+ s.author = author.strip()
+ s.price = '$0.00'
+ s.detail_item = id.strip()
+ s.drm = SearchResult.DRM_UNLOCKED
+ s.formats = 'EPUB'
+
+ yield s
From 7e2e3cbb87fd5d00e285e90826b5155d2771040c Mon Sep 17 00:00:00 2001
From: John Schember
Date: Wed, 1 Jun 2011 18:25:09 -0400
Subject: [PATCH 08/14] Store: EpubBud store, fix detail url
---
src/calibre/gui2/store/epubbud_plugin.py | 3 ---
1 file changed, 3 deletions(-)
diff --git a/src/calibre/gui2/store/epubbud_plugin.py b/src/calibre/gui2/store/epubbud_plugin.py
index 6c20f5150d..d6193f6ae0 100644
--- a/src/calibre/gui2/store/epubbud_plugin.py
+++ b/src/calibre/gui2/store/epubbud_plugin.py
@@ -24,9 +24,6 @@ class EpubBudStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
url = 'http://epubbud.com/'
-
- if detail_item:
- url = 'http://epubbud.com/book.php?g=' + detail_item
if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
From 7de5b6e166dc2a7cc162b33cdcc00794a80da940 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 1 Jun 2011 19:28:36 -0600
Subject: [PATCH 09/14] ...
---
src/calibre/gui2/actions/convert.py | 5 ++---
1 file changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/calibre/gui2/actions/convert.py b/src/calibre/gui2/actions/convert.py
index ed0a064e88..17fa0ad622 100644
--- a/src/calibre/gui2/actions/convert.py
+++ b/src/calibre/gui2/actions/convert.py
@@ -171,10 +171,9 @@ class ConvertAction(InterfaceAction):
raise Exception(_('Empty output file, '
'probably the conversion process crashed'))
- data = open(temp_files[-1].name, 'rb')
- self.gui.library_view.model().db.add_format(book_id, \
+ with open(temp_files[-1].name, 'rb') as data:
+ self.gui.library_view.model().db.add_format(book_id, \
fmt, data, index_is_id=True)
- data.close()
self.gui.status_bar.show_message(job.description + \
(' completed'), 2000)
finally:
From 91d499e5b7e4a59541c97e680bfcddd75574ad5b Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 1 Jun 2011 20:49:38 -0600
Subject: [PATCH 10/14] ...
---
src/calibre/gui2/metadata/single_download.py | 7 +-
src/calibre/gui2/preferences/coloring.py | 225 ++++++++++++++++++-
2 files changed, 217 insertions(+), 15 deletions(-)
diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py
index cc89ef2259..013ab42684 100644
--- a/src/calibre/gui2/metadata/single_download.py
+++ b/src/calibre/gui2/metadata/single_download.py
@@ -35,8 +35,9 @@ from calibre import force_unicode
class RichTextDelegate(QStyledItemDelegate): # {{{
- def __init__(self, parent=None):
+ def __init__(self, parent=None, max_width=160):
QStyledItemDelegate.__init__(self, parent)
+ self.max_width = max_width
def to_doc(self, index):
doc = QTextDocument()
@@ -46,8 +47,8 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
def sizeHint(self, option, index):
doc = self.to_doc(index)
ans = doc.size().toSize()
- if ans.width() > 150:
- ans.setWidth(160)
+ if ans.width() > self.max_width - 10:
+ ans.setWidth(self.max_width)
ans.setHeight(ans.height()+10)
return ans
diff --git a/src/calibre/gui2/preferences/coloring.py b/src/calibre/gui2/preferences/coloring.py
index 98ac7380a5..53f11a95bd 100644
--- a/src/calibre/gui2/preferences/coloring.py
+++ b/src/calibre/gui2/preferences/coloring.py
@@ -7,14 +7,16 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-from PyQt4.Qt import (QWidget, QDialog, QLabel, QGridLayout, QComboBox,
+from PyQt4.Qt import (QWidget, QDialog, QLabel, QGridLayout, QComboBox, QSize,
QLineEdit, QIntValidator, QDoubleValidator, QFrame, QColor, Qt, QIcon,
- QScrollArea, QPushButton, QVBoxLayout, QDialogButtonBox)
+ QScrollArea, QPushButton, QVBoxLayout, QDialogButtonBox, QToolButton,
+ QListView, QAbstractListModel)
from calibre.utils.icu import sort_key
from calibre.gui2 import error_dialog
+from calibre.gui2.metadata.single_download import RichTextDelegate
from calibre.library.coloring import (Rule, conditionable_columns,
- displayable_columns)
+ displayable_columns, rule_from_template)
class ConditionEditor(QWidget): # {{{
@@ -277,6 +279,27 @@ class RuleEditor(QDialog): # {{{
self.conditions.append(c)
self.conditions_widget.layout().addWidget(c)
+ def apply_rule(self, col, rule):
+ for i in range(self.column_box.count()):
+ c = unicode(self.column_box.itemData(i).toString())
+ if col == c:
+ self.column_box.setCurrentIndex(i)
+ break
+ if rule.color:
+ idx = self.color_box.findText(rule.color)
+ if idx >= 0:
+ self.color_box.setCurrentIndex(idx)
+ for c in rule.conditions:
+ ce = ConditionEditor(self.fm, parent=self.conditions_widget)
+ self.conditions.append(ce)
+ self.conditions_widget.layout().addWidget(ce)
+ try:
+ ce.condition = c
+ except:
+ import traceback
+ traceback.print_exc()
+
+
def accept(self):
if self.validate():
QDialog.accept(self)
@@ -316,11 +339,178 @@ class RuleEditor(QDialog): # {{{
return col, r
# }}}
+class RulesModel(QAbstractListModel):
+
+ def __init__(self, prefs, fm, parent=None):
+ QAbstractListModel.__init__(self, parent)
+
+ self.fm = fm
+ rules = list(prefs['column_color_rules'])
+ self.rules = []
+ for col, template in rules:
+ try:
+ rule = rule_from_template(self.fm, template)
+ except:
+ rule = template
+ self.rules.append((col, rule))
+
+ def rowCount(self, *args):
+ return len(self.rules)
+
+ def data(self, index, role):
+ row = index.row()
+ try:
+ col, rule = self.rules[row]
+ except:
+ return None
+
+ if role == Qt.DisplayRole:
+ return self.rule_to_html(col, rule)
+ if role == Qt.UserRole:
+ return (col, rule)
+
+ def add_rule(self, col, rule):
+ self.rules.append((col, rule))
+ self.reset()
+ return self.index(len(self.rules)-1)
+
+ def replace_rule(self, index, col, r):
+ self.rules[index.row()] = (col, r)
+ self.dataChanged.emit(index, index)
+
+ def remove_rule(self, index):
+ self.rules.remove(self.rules[index.row()])
+ self.reset()
+
+ def commit(self, prefs):
+ rules = []
+ for col, r in self.rules:
+ if isinstance(r, Rule):
+ r = r.template
+ if r is not None:
+ rules.append((col, r))
+ prefs['column_color_rules'] = rules
+
+ def rule_to_html(self, col, rule):
+ if isinstance(rule, basestring):
+ return _('''
+ Advanced Rule for column: %s
+
%s
+ ''')%(col, rule)
+ conditions = [self.condition_to_html(c) for c in rule.conditions]
+ return _('''\
+ Set the color of %s to %s if the following
+ conditions are met:
+
+ ''') % (col, rule.color, ''.join(conditions))
+
+ def condition_to_html(self, condition):
+ return (
+ _('If the %s column %s the value: %s') %
+ tuple(condition))
+
class EditRules(QWidget):
- def __init__(self, db, parent=None):
+ def __init__(self, parent=None):
QWidget.__init__(self, parent)
- self.db = db
+
+ self.l = l = QGridLayout(self)
+ self.setLayout(l)
+
+ self.l1 = l1 = QLabel(_(
+ 'You can control the color of columns in the'
+ ' book list by creating "rules" that tell calibre'
+ ' what color to use. Click the Add Rule button below'
+ ' to get started. You can change an existing rule by double'
+ ' clicking it.'))
+ l1.setWordWrap(True)
+ l.addWidget(l1, 0, 0, 1, 2)
+
+ self.add_button = QPushButton(QIcon(I('plus.png')), _('Add Rule'),
+ self)
+ self.remove_button = QPushButton(QIcon(I('minus.png')),
+ _('Remove Rule'), self)
+ self.add_button.clicked.connect(self.add_rule)
+ self.remove_button.clicked.connect(self.remove_rule)
+ l.addWidget(self.add_button, 1, 0)
+ l.addWidget(self.remove_button, 1, 1)
+
+ self.g = g = QGridLayout()
+ self.rules_view = QListView(self)
+ self.rules_view.activated.connect(self.edit_rule)
+ self.rules_view.setSelectionMode(self.rules_view.SingleSelection)
+ self.rules_view.setAlternatingRowColors(True)
+ self.rtfd = RichTextDelegate(parent=self.rules_view, max_width=400)
+ self.rules_view.setItemDelegate(self.rtfd)
+ g.addWidget(self.rules_view, 0, 0, 2, 1)
+
+ self.up_button = b = QToolButton(self)
+ b.setIcon(QIcon(I('arrow-up.png')))
+ b.setToolTip(_('Move the selected rule up'))
+ b.clicked.connect(self.move_up)
+ g.addWidget(b, 0, 1, 1, 1, Qt.AlignTop)
+ self.down_button = b = QToolButton(self)
+ b.setIcon(QIcon(I('arrow-down.png')))
+ b.setToolTip(_('Move the selected rule down'))
+ b.clicked.connect(self.move_down)
+ g.addWidget(b, 1, 1, 1, 1, Qt.AlignBottom)
+
+ l.addLayout(g, 2, 0, 1, 2)
+ l.setRowStretch(2, 10)
+
+ self.add_advanced_button = b = QPushButton(QIcon(I('plus.png')),
+ _('Add Advanced Rule'), self)
+ b.clicked.connect(self.add_advanced)
+ l.addWidget(b, 3, 0, 1, 2)
+
+ def initialize(self, fm, prefs):
+ self.model = RulesModel(prefs, fm)
+ self.rules_view.setModel(self.model)
+
+ def add_rule(self):
+ d = RuleEditor(db.field_metadata)
+ d.add_blank_condition()
+ if d.exec_() == d.Accepted:
+ col, r = d.rule
+ if r is not None and col:
+ idx = self.model.add_rule(col, r)
+ self.rules_view.scrollTo(idx)
+
+ def edit_rule(self, index):
+ try:
+ col, rule = self.model.data(index, Qt.UserRole)
+ except:
+ return
+ if isinstance(rule, Rule):
+ d = RuleEditor(db.field_metadata)
+ d.apply_rule(col, rule)
+ if d.exec_() == d.Accepted:
+ col, r = d.rule
+ if r is not None and col:
+ self.model.replace_rule(index, col, r)
+ self.rules_view.scrollTo(index)
+ else:
+ pass # TODO
+
+ def add_advanced(self):
+ pass
+
+ def remove_rule(self):
+ sm = self.rules_view.selectionModel()
+ rows = list(sm.selectedRows())
+ if not rows:
+ return error_dialog(self, _('No rule selected'),
+ _('No rule selected for removal.'), show=True)
+ self.model.remove_rule(rows[0])
+
+ def move_up(self):
+ pass
+
+ def move_down(self):
+ pass
+
+ def commit(self, prefs):
+ self.model.commit(prefs)
if __name__ == '__main__':
from PyQt4.Qt import QApplication
@@ -328,13 +518,24 @@ if __name__ == '__main__':
from calibre.library import db
- d = RuleEditor(db().field_metadata)
- d.add_blank_condition()
- d.exec_()
+ db = db()
- col, r = d.rule
+ if False:
+ d = RuleEditor(db.field_metadata)
+ d.add_blank_condition()
+ d.exec_()
+
+ col, r = d.rule
+
+ print ('Column to be colored:', col)
+ print ('Template:')
+ print (r.template)
+ else:
+ d = EditRules()
+ d.resize(QSize(800, 600))
+ d.initialize(db.field_metadata, db.prefs)
+ d.show()
+ app.exec_()
+ d.commit(db.prefs)
- print ('Column to be colored:', col)
- print ('Template:')
- print (r.template)
From f6f89636533f0ef512b2c21969161ce4c5341894 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 1 Jun 2011 20:58:00 -0600
Subject: [PATCH 11/14] ...
---
src/calibre/gui2/preferences/coloring.py | 43 ++++++++++++++++++++----
1 file changed, 36 insertions(+), 7 deletions(-)
diff --git a/src/calibre/gui2/preferences/coloring.py b/src/calibre/gui2/preferences/coloring.py
index 53f11a95bd..a8a9f666b9 100644
--- a/src/calibre/gui2/preferences/coloring.py
+++ b/src/calibre/gui2/preferences/coloring.py
@@ -391,6 +391,17 @@ class RulesModel(QAbstractListModel):
rules.append((col, r))
prefs['column_color_rules'] = rules
+ def move(self, idx, delta):
+ row = idx.row() + delta
+ if row >= 0 and row < len(self.rules):
+ t = self.rules[row]
+ self.rules[row] = self.rules[row-delta]
+ self.rules[row-delta] = t
+ self.dataChanged.emit(idx, idx)
+ idx = self.index(row)
+ self.dataChanged.emit(idx, idx)
+ return idx
+
def rule_to_html(self, col, rule):
if isinstance(rule, basestring):
return _('''
@@ -437,7 +448,7 @@ class EditRules(QWidget):
self.g = g = QGridLayout()
self.rules_view = QListView(self)
- self.rules_view.activated.connect(self.edit_rule)
+ self.rules_view.doubleClicked.connect(self.edit_rule)
self.rules_view.setSelectionMode(self.rules_view.SingleSelection)
self.rules_view.setAlternatingRowColors(True)
self.rtfd = RichTextDelegate(parent=self.rules_view, max_width=400)
@@ -495,19 +506,37 @@ class EditRules(QWidget):
def add_advanced(self):
pass
- def remove_rule(self):
+ def get_selected_row(self, txt):
sm = self.rules_view.selectionModel()
rows = list(sm.selectedRows())
if not rows:
- return error_dialog(self, _('No rule selected'),
- _('No rule selected for removal.'), show=True)
- self.model.remove_rule(rows[0])
+ error_dialog(self, _('No rule selected'),
+ _('No rule selected for %s.')%txt, show=True)
+ return None
+ return rows[0]
+
+ def remove_rule(self):
+ row = self.get_selected_row(_('removal'))
+ if row is not None:
+ self.model.remove_rule(row)
def move_up(self):
- pass
+ idx = self.rules_view.currentIndex()
+ if idx.isValid():
+ idx = self.model.move(idx, -1)
+ if idx is not None:
+ sm = self.rules_view.selectionModel()
+ sm.select(idx, sm.ClearAndSelect)
+ self.rules_view.setCurrentIndex(idx)
def move_down(self):
- pass
+ idx = self.rules_view.currentIndex()
+ if idx.isValid():
+ idx = self.model.move(idx, 1)
+ if idx is not None:
+ sm = self.rules_view.selectionModel()
+ sm.select(idx, sm.ClearAndSelect)
+ self.rules_view.setCurrentIndex(idx)
def commit(self, prefs):
self.model.commit(prefs)
From 6b8a1442c1249fb2d5c9e3e18657367748b50d20 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 1 Jun 2011 21:47:03 -0600
Subject: [PATCH 12/14] New preferences interface for column coloring. Note
that editing of advanced rules is not yet implemented.
---
src/calibre/gui2/library/models.py | 22 +--
src/calibre/gui2/preferences/coloring.py | 36 ++++-
src/calibre/gui2/preferences/look_feel.py | 134 ++----------------
src/calibre/gui2/preferences/look_feel.ui | 162 +---------------------
src/calibre/library/coloring.py | 15 +-
src/calibre/library/database2.py | 22 ++-
6 files changed, 79 insertions(+), 312 deletions(-)
diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py
index d79c92befa..72c8e0629f 100644
--- a/src/calibre/gui2/library/models.py
+++ b/src/calibre/gui2/library/models.py
@@ -99,8 +99,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.ids_to_highlight_set = set()
self.current_highlighted_idx = None
self.highlight_only = False
- self.column_color_list = []
- self.colors = [unicode(c) for c in QColor.colorNames()]
+ self.colors = frozenset([unicode(c) for c in QColor.colorNames()])
self.read_config()
def change_alignment(self, colname, alignment):
@@ -156,7 +155,6 @@ class BooksModel(QAbstractTableModel): # {{{
self.headers[col] = self.custom_columns[col]['name']
self.build_data_convertors()
- self.set_color_templates(reset=False)
self.reset()
self.database_changed.emit(db)
self.stop_metadata_backup()
@@ -545,16 +543,6 @@ class BooksModel(QAbstractTableModel): # {{{
img = self.default_image
return img
- def set_color_templates(self, reset=True):
- self.column_color_list = []
- for i in range(1,self.db.column_color_count+1):
- name = self.db.prefs.get('column_color_name_'+str(i))
- if name:
- self.column_color_list.append((name,
- self.db.prefs.get('column_color_template_'+str(i))))
- if reset:
- self.reset()
-
def build_data_convertors(self):
def authors(r, idx=-1):
au = self.db.data[r][idx]
@@ -726,14 +714,16 @@ class BooksModel(QAbstractTableModel): # {{{
return QVariant(QColor('lightgreen'))
elif role == Qt.ForegroundRole:
key = self.column_map[col]
- for k,fmt in self.column_color_list:
+ mi = None
+ for k, fmt in self.db.prefs['column_color_rules']:
if k != key:
continue
id_ = self.id(index)
if id_ in self.color_cache:
if key in self.color_cache[id_]:
return self.color_cache[id_][key]
- mi = self.db.get_metadata(self.id(index), index_is_id=True)
+ if mi is None:
+ mi = self.db.get_metadata(id_, index_is_id=True)
try:
color = composite_formatter.safe_format(fmt, mi, '', mi)
if color in self.colors:
@@ -743,7 +733,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.color_cache[id_][key] = color
return color
except:
- return NONE
+ continue
if self.is_custom_column(key) and \
self.custom_columns[key]['datatype'] == 'enumeration':
cc = self.custom_columns[self.column_map[col]]['display']
diff --git a/src/calibre/gui2/preferences/coloring.py b/src/calibre/gui2/preferences/coloring.py
index a8a9f666b9..ec5fef1304 100644
--- a/src/calibre/gui2/preferences/coloring.py
+++ b/src/calibre/gui2/preferences/coloring.py
@@ -10,10 +10,11 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import (QWidget, QDialog, QLabel, QGridLayout, QComboBox, QSize,
QLineEdit, QIntValidator, QDoubleValidator, QFrame, QColor, Qt, QIcon,
QScrollArea, QPushButton, QVBoxLayout, QDialogButtonBox, QToolButton,
- QListView, QAbstractListModel)
+ QListView, QAbstractListModel, pyqtSignal)
from calibre.utils.icu import sort_key
from calibre.gui2 import error_dialog
+from calibre.gui2.dialogs.template_dialog import TemplateDialog
from calibre.gui2.metadata.single_download import RichTextDelegate
from calibre.library.coloring import (Rule, conditionable_columns,
displayable_columns, rule_from_template)
@@ -402,6 +403,10 @@ class RulesModel(QAbstractListModel):
self.dataChanged.emit(idx, idx)
return idx
+ def clear(self):
+ self.rules = []
+ self.reset()
+
def rule_to_html(self, col, rule):
if isinstance(rule, basestring):
return _('''
@@ -422,6 +427,8 @@ class RulesModel(QAbstractListModel):
class EditRules(QWidget):
+ changed = pyqtSignal()
+
def __init__(self, parent=None):
QWidget.__init__(self, parent)
@@ -479,13 +486,20 @@ class EditRules(QWidget):
self.rules_view.setModel(self.model)
def add_rule(self):
- d = RuleEditor(db.field_metadata)
+ d = RuleEditor(self.model.fm)
d.add_blank_condition()
if d.exec_() == d.Accepted:
col, r = d.rule
if r is not None and col:
idx = self.model.add_rule(col, r)
self.rules_view.scrollTo(idx)
+ self.changed.emit()
+
+ def add_advanced(self):
+ td = TemplateDialog(self, '', None)
+ if td.exec_() == td.Accepted:
+ self.changed.emit()
+ pass # TODO
def edit_rule(self, index):
try:
@@ -493,18 +507,19 @@ class EditRules(QWidget):
except:
return
if isinstance(rule, Rule):
- d = RuleEditor(db.field_metadata)
+ d = RuleEditor(self.model.fm)
d.apply_rule(col, rule)
if d.exec_() == d.Accepted:
col, r = d.rule
if r is not None and col:
self.model.replace_rule(index, col, r)
self.rules_view.scrollTo(index)
+ self.changed.emit()
else:
- pass # TODO
-
- def add_advanced(self):
- pass
+ td = TemplateDialog(self, rule, None)
+ if td.exec_() == td.Accepted:
+ self.changed.emit()
+ pass # TODO
def get_selected_row(self, txt):
sm = self.rules_view.selectionModel()
@@ -519,6 +534,7 @@ class EditRules(QWidget):
row = self.get_selected_row(_('removal'))
if row is not None:
self.model.remove_rule(row)
+ self.changed.emit()
def move_up(self):
idx = self.rules_view.currentIndex()
@@ -528,6 +544,7 @@ class EditRules(QWidget):
sm = self.rules_view.selectionModel()
sm.select(idx, sm.ClearAndSelect)
self.rules_view.setCurrentIndex(idx)
+ self.changed.emit()
def move_down(self):
idx = self.rules_view.currentIndex()
@@ -537,6 +554,11 @@ class EditRules(QWidget):
sm = self.rules_view.selectionModel()
sm.select(idx, sm.ClearAndSelect)
self.rules_view.setCurrentIndex(idx)
+ self.changed.emit()
+
+ def clear(self):
+ self.model.clear()
+ self.changed.emit()
def commit(self, prefs):
self.model.commit(prefs)
diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py
index 7a8c1fb69c..feaf3dd677 100644
--- a/src/calibre/gui2/preferences/look_feel.py
+++ b/src/calibre/gui2/preferences/look_feel.py
@@ -5,21 +5,19 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-from functools import partial
-
from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog,
- QAbstractListModel, Qt, QColor, QIcon, QToolButton, QComboBox)
+ QAbstractListModel, Qt, QIcon)
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
from calibre.gui2.preferences.look_feel_ui import Ui_Form
from calibre.gui2 import config, gprefs, qt_app
-from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor
from calibre.utils.localization import (available_translations,
get_language, get_lang)
from calibre.utils.config import prefs
from calibre.utils.icu import sort_key
from calibre.gui2 import NONE
from calibre.gui2.book_details import get_field_list
+from calibre.gui2.preferences.coloring import EditRules
class DisplayedFields(QAbstractListModel): # {{{
@@ -162,117 +160,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.df_up_button.clicked.connect(self.move_df_up)
self.df_down_button.clicked.connect(self.move_df_down)
- self.color_help_text.setText('' +
- _('Here you can specify coloring rules for columns shown in the '
- 'library view. Choose the column you wish to color, then '
- 'supply a template that specifies the color to use based on '
- 'the values in the column. There is a '
- ''
- 'tutorial on using templates.') +
- '
' +
- _('If you want to color a field based on contents of columns, '
- 'then click the button next to an empty line to open the wizard. '
- 'It will build a template for you. You can later edit that '
- 'template with the same wizard. This is by far the easiest '
- 'way to specify a template.') +
- '
' +
- _('If you manually construct a template, then the template must '
- 'evaluate to a valid color name shown in the color names box.'
- 'You can use any legal template expression. '
- 'For example, you can set the title to always display in '
- 'green using the template "green" (without the quotes). '
- 'To show the title in the color named in the custom column '
- '#column, use "{#column}". To show the title in blue if the '
- 'custom column #column contains the value "foo", in red if the '
- 'column contains the value "bar", otherwise in black, use '
- '
{#column:switch(foo,blue,bar,red,black)}
'
- 'To show the title in blue if the book has the exact tag '
- '"Science Fiction", red if the book has the exact tag '
- '"Mystery", or black if the book has neither tag, use'
- "program: \n"
- " t = field('tags'); \n"
- " first_non_empty(\n"
- " in_list(t, ',', '^Science Fiction$', 'blue', ''), \n"
- " in_list(t, ',', '^Mystery$', 'red', 'black'))
"
- 'To show the title in green if it has one format, blue if it '
- 'two formats, and red if more, use'
- "program:cmp(count(field('formats'),','), 2, 'green', 'blue', 'red')
") +
- '
' +
- _('You can access a multi-line template editor from the '
- 'context menu (right-click).') + '
' +
- _('Note: if you want to color a "custom column with a fixed set '
- 'of values", it is often easier to specify the '
- 'colors in the column definition dialog. There you can '
- 'provide a color for each value without using a template.')+ '
')
- self.color_help_scrollArea.setVisible(False)
- self.color_help_button.clicked.connect(self.change_help_text)
- self.colors_scrollArea.setVisible(False)
- self.colors_label.setVisible(False)
- self.colors_button.clicked.connect(self.change_colors_text)
-
- choices = db.field_metadata.displayable_field_keys()
- choices.sort(key=sort_key)
- choices.insert(0, '')
- self.column_color_count = db.column_color_count+1
-
- mi=None
- try:
- idx = gui.library_view.currentIndex().row()
- mi = db.get_metadata(idx, index_is_id=False)
- except:
- pass
-
- l = self.column_color_layout
- for i in range(1, self.column_color_count):
- ccn = QComboBox(parent=self)
- setattr(self, 'opt_column_color_name_'+str(i), ccn)
- l.addWidget(ccn, i, 0, 1, 1)
-
- wtb = QToolButton(parent=self)
- setattr(self, 'opt_column_color_wizard_'+str(i), wtb)
- wtb.setIcon(QIcon(I('wizard.png')))
- l.addWidget(wtb, i, 1, 1, 1)
-
- ttb = QToolButton(parent=self)
- setattr(self, 'opt_column_color_tpledit_'+str(i), ttb)
- ttb.setIcon(QIcon(I('edit_input.png')))
- l.addWidget(ttb, i, 2, 1, 1)
-
- tpl = TemplateLineEditor(parent=self)
- setattr(self, 'opt_column_color_template_'+str(i), tpl)
- tpl.textChanged.connect(partial(self.tpl_edit_text_changed, ctrl=i))
- tpl.set_db(db)
- tpl.set_mi(mi)
- l.addWidget(tpl, i, 3, 1, 1)
-
- wtb.clicked.connect(tpl.tag_wizard)
- ttb.clicked.connect(tpl.open_editor)
-
- r('column_color_name_'+str(i), db.prefs, choices=choices)
- r('column_color_template_'+str(i), db.prefs)
- txt = db.prefs.get('column_color_template_'+str(i), None)
-
- wtb.setEnabled(tpl.enable_wizard_button(txt))
- ttb.setEnabled(not tpl.enable_wizard_button(txt) or not txt)
-
- all_colors = [unicode(s) for s in list(QColor.colorNames())]
- self.colors_box.setText(', '.join(all_colors))
-
- def change_help_text(self):
- self.color_help_scrollArea.setVisible(not self.color_help_scrollArea.isVisible())
-
- def change_colors_text(self):
- self.colors_scrollArea.setVisible(not self.colors_scrollArea.isVisible())
- self.colors_label.setVisible(not self.colors_label.isVisible())
-
- def tpl_edit_text_changed(self, ign, ctrl=None):
- tpl = getattr(self, 'opt_column_color_template_'+str(ctrl))
- txt = unicode(tpl.text())
- wtb = getattr(self, 'opt_column_color_wizard_'+str(ctrl))
- ttb = getattr(self, 'opt_column_color_tpledit_'+str(ctrl))
- wtb.setEnabled(tpl.enable_wizard_button(txt))
- ttb.setEnabled(not tpl.enable_wizard_button(txt) or not txt)
- tpl.setFocus()
+ self.edit_rules = EditRules(self.tabWidget)
+ self.edit_rules.changed.connect(self.changed_signal)
+ self.tabWidget.addTab(self.edit_rules,
+ QIcon(I('format-fill-color.png')), _('Column coloring'))
+ self.tabWidget.setCurrentIndex(0)
def initialize(self):
ConfigWidgetBase.initialize(self)
@@ -283,6 +175,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.current_font = self.initial_font = font
self.update_font_display()
self.display_model.initialize()
+ db = self.gui.current_db
+ self.edit_rules.initialize(db.field_metadata, db.prefs)
def restore_defaults(self):
ConfigWidgetBase.restore_defaults(self)
@@ -292,6 +186,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.changed_signal.emit()
self.update_font_display()
self.display_model.restore_defaults()
+ self.edit_rules.clear()
self.changed_signal.emit()
def build_font_obj(self):
@@ -341,12 +236,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.changed_signal.emit()
def commit(self, *args):
- for i in range(1, self.column_color_count):
- col = getattr(self, 'opt_column_color_name_'+str(i))
- tpl = getattr(self, 'opt_column_color_template_'+str(i))
- if not col.currentIndex() or not unicode(tpl.text()).strip():
- col.setCurrentIndex(0)
- tpl.setText('')
rr = ConfigWidgetBase.commit(self, *args)
if self.current_font != self.initial_font:
gprefs['font'] = (self.current_font[:4] if self.current_font else
@@ -356,10 +245,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
QApplication.setFont(self.font_display.font())
rr = True
self.display_model.commit()
+ self.edit_rules.commit(self.gui.current_db.prefs)
return rr
def refresh_gui(self, gui):
- gui.library_view.model().set_color_templates()
+ gui.library_view.model().reset()
self.update_font_display()
gui.tags_view.reread_collapse_parameters()
gui.library_view.refresh_book_details()
diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui
index def1bdd41c..cc9133a36f 100644
--- a/src/calibre/gui2/preferences/look_feel.ui
+++ b/src/calibre/gui2/preferences/look_feel.ui
@@ -6,7 +6,7 @@
0
0
- 717
+ 820
519
@@ -407,161 +407,6 @@ then the tags will be displayed each on their own line.
-
-
-
- :/images/format-fill-color.png:/images/format-fill-color.png
-
-
- Column Coloring
-
-
- -
-
-
- Column to color
-
-
-
- -
-
-
-
-
-
- Color selection template
-
-
-
- 10
- 0
-
-
-
-
- -
-
-
- The template wizard is easiest to use
-
-
-
- -
-
-
- Show/hide help text
-
-
-
- -
-
-
- Show/hide colors
-
-
-
-
-
- -
-
-
- Color names
-
-
-
- -
-
-
-
- 16777215
- 300
-
-
-
- true
-
-
-
-
- 0
- 0
- 687
- 61
-
-
-
-
-
-
-
- true
-
-
- Qt::AlignLeft|Qt::AlignTop
-
-
-
-
-
-
-
- -
-
-
-
- 0
- 200
-
-
-
- true
-
-
- Qt::AlignLeft|Qt::AlignTop
-
-
-
-
- 0
- 0
- 687
- 194
-
-
-
-
-
-
-
- true
-
-
- true
-
-
-
-
-
-
-
- -
-
-
-
- 0
- 10
-
-
-
- Qt::Vertical
-
-
-
- 0
- 0
-
-
-
-
-
-
@@ -572,11 +417,6 @@ then the tags will be displayed each on their own line.
QLineEdit
-
- TemplateLineEditor
- QLineEdit
- calibre/gui2/dialogs/template_line_editor.h
-
diff --git a/src/calibre/library/coloring.py b/src/calibre/library/coloring.py
index 7e2b0f67c6..80e748473a 100644
--- a/src/calibre/library/coloring.py
+++ b/src/calibre/library/coloring.py
@@ -61,7 +61,7 @@ class Rule(object): # {{{
{sig}
test(and(
{conditions}
- ), {color}, '');
+ ), '{color}', '');
''').format(sig=self.signature, conditions=conditions,
color=self.color)
@@ -169,10 +169,21 @@ def conditionable_columns(fm):
'comments', 'text', 'enumeration', 'datetime'):
yield key
-
def displayable_columns(fm):
for key in fm.displayable_field_keys():
if key not in ('sort', 'author_sort', 'comments', 'formats',
'identifiers', 'path'):
yield key
+def migrate_old_rule(fm, template):
+ if template.startswith('program:\n#tag wizard'):
+ rules = []
+ for line in template.splitlines():
+ if line.startswith('#') and ':|:' in line:
+ value, color = line[1:].split(':|:')
+ r = Rule(fm, color=color)
+ r.add_condition('tags', 'has', value)
+ rules.append(r.template)
+ return rules
+ return template
+
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index df465c919e..b3c584534e 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -211,10 +211,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
defs = self.prefs.defaults
defs['gui_restriction'] = defs['cs_restriction'] = ''
defs['categories_using_hierarchy'] = []
- self.column_color_count = 5
- for i in range(1,self.column_color_count+1):
- defs['column_color_name_'+str(i)] = ''
- defs['column_color_template_'+str(i)] = ''
+ defs['column_color_rules'] = []
# Migrate the bool tristate tweak
defs['bools_are_tristate'] = \
@@ -222,6 +219,23 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if self.prefs.get('bools_are_tristate') is None:
self.prefs.set('bools_are_tristate', defs['bools_are_tristate'])
+ # Migrate column coloring rules
+ if self.prefs.get('column_color_name_1', None) is not None:
+ from calibre.library.coloring import migrate_old_rule
+ old_rules = []
+ for i in range(1, 5):
+ col = self.prefs.get('column_color_name_'+str(i), None)
+ templ = self.prefs.get('column_color_template_'+str(i), None)
+ if col and templ:
+ try:
+ del self.prefs['column_color_name_'+str(i)]
+ templ = migrate_old_rule(self.field_metadata, templ)
+ old_rules.append((col, templ))
+ except:
+ pass
+ if old_rules:
+ self.prefs['column_color_rules'] += old_rules
+
# Migrate saved search and user categories to db preference scheme
def migrate_preference(key, default):
oldval = prefs[key]
From cf037a16fc0356b2851db251cd494a0be1ee740a Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 1 Jun 2011 21:50:30 -0600
Subject: [PATCH 13/14] ...
---
src/calibre/gui2/preferences/coloring.py | 8 ++++++--
1 file changed, 6 insertions(+), 2 deletions(-)
diff --git a/src/calibre/gui2/preferences/coloring.py b/src/calibre/gui2/preferences/coloring.py
index ec5fef1304..a8825ec582 100644
--- a/src/calibre/gui2/preferences/coloring.py
+++ b/src/calibre/gui2/preferences/coloring.py
@@ -340,7 +340,7 @@ class RuleEditor(QDialog): # {{{
return col, r
# }}}
-class RulesModel(QAbstractListModel):
+class RulesModel(QAbstractListModel): # {{{
def __init__(self, prefs, fm, parent=None):
QAbstractListModel.__init__(self, parent)
@@ -425,7 +425,9 @@ class RulesModel(QAbstractListModel):
_('If the %s column %s the value: %s') %
tuple(condition))
-class EditRules(QWidget):
+# }}}
+
+class EditRules(QWidget): # {{{
changed = pyqtSignal()
@@ -563,6 +565,8 @@ class EditRules(QWidget):
def commit(self, prefs):
self.model.commit(prefs)
+# }}}
+
if __name__ == '__main__':
from PyQt4.Qt import QApplication
app = QApplication([])
From 2010cf26710b78d20fa01056bccc74cda7fe2fef Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 1 Jun 2011 21:54:23 -0600
Subject: [PATCH 14/14] ...
---
src/calibre/library/coloring.py | 2 +-
src/calibre/library/database2.py | 5 +++--
2 files changed, 4 insertions(+), 3 deletions(-)
diff --git a/src/calibre/library/coloring.py b/src/calibre/library/coloring.py
index 80e748473a..db13da9532 100644
--- a/src/calibre/library/coloring.py
+++ b/src/calibre/library/coloring.py
@@ -185,5 +185,5 @@ def migrate_old_rule(fm, template):
r.add_condition('tags', 'has', value)
rules.append(r.template)
return rules
- return template
+ return [template]
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index b3c584534e..c78f13d698 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -229,8 +229,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if col and templ:
try:
del self.prefs['column_color_name_'+str(i)]
- templ = migrate_old_rule(self.field_metadata, templ)
- old_rules.append((col, templ))
+ rules = migrate_old_rule(self.field_metadata, templ)
+ for templ in rules:
+ old_rules.append((col, templ))
except:
pass
if old_rules: