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:

+
    %s
+ ''') % (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
    calibre/gui2/complete.h
    - - 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: