From c3a2c3f458304abaef826e08f61265a235df5d50 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 21 May 2011 19:24:04 +0100 Subject: [PATCH 01/14] Add option to color custom enumeration values in the library view --- src/calibre/gui2/library/delegates.py | 25 ++++++++++++++++ .../gui2/preferences/create_custom_column.py | 21 ++++++++++++-- .../gui2/preferences/create_custom_column.ui | 29 ++++++++++++++++--- 3 files changed, 68 insertions(+), 7 deletions(-) diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index e2234f6df5..ae01081736 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -274,6 +274,31 @@ class CcEnumDelegate(QStyledItemDelegate): # {{{ Delegate for text/int/float data. ''' + def __init__(self, parent): + QStyledItemDelegate.__init__(self, parent) + self.document = QTextDocument() + + def paint(self, painter, option, index): + style = self.parent().style() + txt = unicode(index.data(Qt.DisplayRole).toString()) + self.document.setPlainText(txt) + painter.save() + if hasattr(QStyle, 'CE_ItemViewItem'): + style.drawControl(QStyle.CE_ItemViewItem, option, + painter, self.parent()) + elif option.state & QStyle.State_Selected: + painter.fillRect(option.rect, option.palette.highlight()) + m = index.model() + col = m.column_map[index.column()] + colors = m.custom_columns[col]['display'].get('enum_colors', []) + values = m.custom_columns[col]['display']['enum_values'] + if len(colors) > 0 and txt in values: + painter.fillRect(option.rect, QColor(colors[values.index(txt)])) + painter.setClipRect(option.rect) + painter.translate(option.rect.topLeft()) + self.document.drawContents(painter) + painter.restore() + def createEditor(self, parent, option, index): m = index.model() col = m.column_map[index.column()] diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py index 7b891b782c..180c3aed7a 100644 --- a/src/calibre/gui2/preferences/create_custom_column.py +++ b/src/calibre/gui2/preferences/create_custom_column.py @@ -6,7 +6,7 @@ __copyright__ = '2010, Kovid Goyal ' import re from functools import partial -from PyQt4.Qt import QDialog, Qt, QListWidgetItem, QVariant +from PyQt4.Qt import QDialog, Qt, QListWidgetItem, QVariant, QColor from calibre.gui2.preferences.create_custom_column_ui import Ui_QCreateCustomColumn from calibre.gui2 import error_dialog @@ -126,6 +126,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): c['display'].get('make_category', False)) elif ct == 'enumeration': self.enum_box.setText(','.join(c['display'].get('enum_values', []))) + self.enum_colors.setText(','.join(c['display'].get('enum_colors', []))) self.datatype_changed() if ct in ['text', 'composite', 'enumeration']: self.use_decorations.setChecked(c['display'].get('use_decorations', False)) @@ -170,7 +171,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): for x in ('box', 'default_label', 'label', 'sort_by', 'sort_by_label', 'make_category'): getattr(self, 'composite_'+x).setVisible(col_type in ['composite', '*composite']) - for x in ('box', 'default_label', 'label'): + for x in ('box', 'default_label', 'label', 'colors', 'colors_label'): getattr(self, 'enum_'+x).setVisible(col_type == 'enumeration') self.use_decorations.setVisible(col_type in ['text', 'composite', 'enumeration']) self.is_names.setVisible(col_type == '*text') @@ -247,7 +248,21 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): if l[i] in l[i+1:]: return self.simple_error('', _('The value "{0}" is in the ' 'list more than once').format(l[i])) - display_dict = {'enum_values': l} + c = unicode(self.enum_colors.text()) + if c: + c = [v.strip() for v in unicode(self.enum_colors.text()).split(',')] + else: + c = [] + print c, len(c) + if len(c) != 0 and len(c) != len(l): + return self.simple_error('', _('The colors box must be empty or ' + 'contain the same number of items as the value box')) + for tc in c: + if tc not in QColor.colorNames(): + return self.simple_error('', + _('The color {0} is unknown').format(tc)) + + display_dict = {'enum_values': l, 'enum_colors': c} elif col_type == 'text' and is_multiple: display_dict = {'is_names': self.is_names.isChecked()} diff --git a/src/calibre/gui2/preferences/create_custom_column.ui b/src/calibre/gui2/preferences/create_custom_column.ui index 619b0c6212..2bdadd4b9d 100644 --- a/src/calibre/gui2/preferences/create_custom_column.ui +++ b/src/calibre/gui2/preferences/create_custom_column.ui @@ -304,8 +304,8 @@ Everything else will show nothing. - - + + @@ -320,13 +320,34 @@ four values, the first of them being the empty value. - + The empty string is always the first value - Default: (nothing) + Values + + + + + + + + 0 + 0 + + + + A list of color names to use when displaying an item. The +list must be empty or contain a color for each value. + + + + + + + Colors From 994974fb59afea4e781c99b0019c4d425f4ee714 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 21 May 2011 19:34:23 +0100 Subject: [PATCH 02/14] Add tooltip listing all colors available --- src/calibre/gui2/preferences/create_custom_column.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/preferences/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py index 180c3aed7a..3a245580dd 100644 --- a/src/calibre/gui2/preferences/create_custom_column.py +++ b/src/calibre/gui2/preferences/create_custom_column.py @@ -132,6 +132,9 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): self.use_decorations.setChecked(c['display'].get('use_decorations', False)) elif ct == '*text': self.is_names.setChecked(c['display'].get('is_names', False)) + + all_colors = [unicode(s) for s in list(QColor.colorNames())] + self.enum_colors_label.setToolTip('

' + ', '.join(all_colors) + '

') self.exec_() def shortcut_activated(self, url): @@ -253,7 +256,6 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn): c = [v.strip() for v in unicode(self.enum_colors.text()).split(',')] else: c = [] - print c, len(c) if len(c) != 0 and len(c) != len(l): return self.simple_error('', _('The colors box must be empty or ' 'contain the same number of items as the value box')) From 6eec4fa410eddb376b62d064549460ee754031bb Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sat, 21 May 2011 19:40:27 +0100 Subject: [PATCH 03/14] More robust coloring code in the CC Enum Delegate --- src/calibre/gui2/library/delegates.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index ae01081736..1af0482a31 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -289,11 +289,14 @@ class CcEnumDelegate(QStyledItemDelegate): # {{{ elif option.state & QStyle.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) m = index.model() - col = m.column_map[index.column()] - colors = m.custom_columns[col]['display'].get('enum_colors', []) - values = m.custom_columns[col]['display']['enum_values'] + cc = m.custom_columns[m.column_map[index.column()]]['display'] + colors = cc.get('enum_colors', []) + values = cc.get('enum_values', []) if len(colors) > 0 and txt in values: - painter.fillRect(option.rect, QColor(colors[values.index(txt)])) + try: + painter.fillRect(option.rect, QColor(colors[values.index(txt)])) + except: + pass painter.setClipRect(option.rect) painter.translate(option.rect.topLeft()) self.document.drawContents(painter) From c3688278d0ac265fa4d53a084ca1b855300c9dcc Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 22 May 2011 13:00:30 +0100 Subject: [PATCH 04/14] First cut at template-based column coloring --- .../gui2/dialogs/template_line_editor.py | 31 +++++++ src/calibre/gui2/init.py | 1 + src/calibre/gui2/library/delegates.py | 79 +++++++----------- src/calibre/gui2/library/models.py | 36 +++++++- src/calibre/gui2/preferences/look_feel.py | 8 ++ src/calibre/gui2/preferences/look_feel.ui | 83 +++++++++++++++++++ src/calibre/library/database2.py | 4 + 7 files changed, 191 insertions(+), 51 deletions(-) create mode 100644 src/calibre/gui2/dialogs/template_line_editor.py diff --git a/src/calibre/gui2/dialogs/template_line_editor.py b/src/calibre/gui2/dialogs/template_line_editor.py new file mode 100644 index 0000000000..d7ba8e4900 --- /dev/null +++ b/src/calibre/gui2/dialogs/template_line_editor.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import (SIGNAL, QLineEdit) +from calibre.gui2.dialogs.template_dialog import TemplateDialog + +class LineEditWithTextBox(QLineEdit): + + ''' + Extend the context menu of a QLineEdit to include more actions. + ''' + + def contextMenuEvent(self, event): + menu = self.createStandardContextMenu() + menu.addSeparator() + + action_open_editor = menu.addAction(_('Open Editor')) + + self.connect(action_open_editor, SIGNAL('triggered()'), self.open_editor) + menu.exec_(event.globalPos()) + + def open_editor(self): + t = TemplateDialog(self, self.text()) + if t.exec_(): + self.setText(t.textbox.toPlainText()) + + diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index a75ff01b21..079e1814c3 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -65,6 +65,7 @@ class LibraryViewMixin(object): # {{{ self.build_context_menus() self.library_view.model().set_highlight_only(config['highlight_search_matches']) + self.library_view.model().set_color_templates() def build_context_menus(self): lm = QMenu(self) diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index 1af0482a31..042e568d8b 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -7,11 +7,12 @@ __docformat__ = 'restructuredtext en' from math import cos, sin, pi -from PyQt4.Qt import QColor, Qt, QModelIndex, QSize, \ - QPainterPath, QLinearGradient, QBrush, \ +from PyQt4.Qt import QColor, Qt, QModelIndex, QSize, QPalette, \ + QPainterPath, QLinearGradient, QBrush, QApplication, \ QPen, QStyle, QPainter, QStyleOptionViewItemV4, \ QIcon, QDoubleSpinBox, QVariant, QSpinBox, \ - QStyledItemDelegate, QComboBox, QTextDocument + QStyledItemDelegate, QComboBox, QTextDocument, \ + QAbstractTextDocumentLayout from calibre.gui2 import UNDEFINED_QDATE, error_dialog from calibre.gui2.widgets import EnLineEdit @@ -51,9 +52,7 @@ class RatingDelegate(QStyledItemDelegate): # {{{ return QSize(5*(self.SIZE), self.SIZE+4) def paint(self, painter, option, index): - style = self._parent.style() - option = QStyleOptionViewItemV4(option) - self.initStyleOption(option, self.dummy) + self.initStyleOption(option, index) num = index.model().data(index, Qt.DisplayRole).toInt()[0] def draw_star(): painter.save() @@ -65,18 +64,24 @@ class RatingDelegate(QStyledItemDelegate): # {{{ painter.restore() painter.save() - if hasattr(QStyle, 'CE_ItemViewItem'): - style.drawControl(QStyle.CE_ItemViewItem, option, - painter, self._parent) - elif option.state & QStyle.State_Selected: + if option.state & QStyle.State_Selected: painter.fillRect(option.rect, option.palette.highlight()) + else: + painter.fillRect(option.rect, option.backgroundBrush) + try: painter.setRenderHint(QPainter.Antialiasing) painter.setClipRect(option.rect) y = option.rect.center().y()-self.SIZE/2. x = option.rect.left() - painter.setPen(self.PEN) - painter.setBrush(self.brush) + brush = index.model().data(index, role=Qt.ForegroundRole) + if brush is None: + pen = self.PEN + painter.setBrush(self.COLOR) + else: + pen = QPen(brush, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) + painter.setBrush(brush) + painter.setPen(pen) painter.translate(x, y) i = 0 while i < num: @@ -274,34 +279,6 @@ class CcEnumDelegate(QStyledItemDelegate): # {{{ Delegate for text/int/float data. ''' - def __init__(self, parent): - QStyledItemDelegate.__init__(self, parent) - self.document = QTextDocument() - - def paint(self, painter, option, index): - style = self.parent().style() - txt = unicode(index.data(Qt.DisplayRole).toString()) - self.document.setPlainText(txt) - painter.save() - if hasattr(QStyle, 'CE_ItemViewItem'): - style.drawControl(QStyle.CE_ItemViewItem, option, - painter, self.parent()) - elif option.state & QStyle.State_Selected: - painter.fillRect(option.rect, option.palette.highlight()) - m = index.model() - cc = m.custom_columns[m.column_map[index.column()]]['display'] - colors = cc.get('enum_colors', []) - values = cc.get('enum_values', []) - if len(colors) > 0 and txt in values: - try: - painter.fillRect(option.rect, QColor(colors[values.index(txt)])) - except: - pass - painter.setClipRect(option.rect) - painter.translate(option.rect.topLeft()) - self.document.drawContents(painter) - painter.restore() - def createEditor(self, parent, option, index): m = index.model() col = m.column_map[index.column()] @@ -339,17 +316,19 @@ class CcCommentsDelegate(QStyledItemDelegate): # {{{ self.document = QTextDocument() def paint(self, painter, option, index): - style = self.parent().style() - self.document.setHtml(index.data(Qt.DisplayRole).toString()) + self.initStyleOption(option, index) + style = QApplication.style() if option.widget is None \ + else option.widget.style() + self.document.setHtml(option.text) + option.text = "" + style.drawControl(QStyle.CE_ItemViewItem, option, painter); + ctx = QAbstractTextDocumentLayout.PaintContext() + ctx.palette = option.palette #.setColor(QPalette.Text, QColor("red")); + textRect = style.subElementRect(QStyle.SE_ItemViewItemText, option) painter.save() - if hasattr(QStyle, 'CE_ItemViewItem'): - style.drawControl(QStyle.CE_ItemViewItem, option, - painter, self.parent()) - elif option.state & QStyle.State_Selected: - painter.fillRect(option.rect, option.palette.highlight()) - painter.setClipRect(option.rect) - painter.translate(option.rect.topLeft()) - self.document.drawContents(painter) + painter.translate(textRect.topLeft()) + painter.setClipRect(textRect.translated(-textRect.topLeft())) + self.document.documentLayout().draw(painter, ctx) painter.restore() def createEditor(self, parent, option, index): diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index fc1117167d..7d6cfadacb 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -14,6 +14,7 @@ from PyQt4.Qt import (QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, from calibre.gui2 import NONE, UNDEFINED_QDATE from calibre.utils.pyparsing import ParseException from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors +from calibre.ebooks.metadata.book.base import composite_formatter from calibre.ptempfile import PersistentTemporaryFile from calibre.utils.config import tweaks, prefs from calibre.utils.date import dt_factory, qt_to_dt @@ -96,6 +97,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.ids_to_highlight_set = set() self.current_highlighted_idx = None self.highlight_only = False + self.column_color_map = {} self.read_config() def change_alignment(self, colname, alignment): @@ -532,6 +534,16 @@ class BooksModel(QAbstractTableModel): # {{{ img = self.default_image return img + def set_color_templates(self): + print 'here' + self.column_color_map = {} + for i in range(1,self.db.column_color_count+1): + name = self.db.prefs.get('column_color_name_'+str(i)) + if name: + print name, self.db.prefs.get('column_color_template_'+str(i)) + self.column_color_map[name] = \ + self.db.prefs.get('column_color_template_'+str(i)) + self.refresh() def build_data_convertors(self): def authors(r, idx=-1): @@ -693,9 +705,31 @@ class BooksModel(QAbstractTableModel): # {{{ return NONE if role in (Qt.DisplayRole, Qt.EditRole): return self.column_to_dc_map[col](index.row()) - elif role == Qt.BackgroundColorRole: + elif role == Qt.BackgroundRole: if self.id(index) in self.ids_to_highlight_set: return QColor('lightgreen') + elif role == Qt.ForegroundRole: + key = self.column_map[col] + if key in self.column_color_map: + mi = self.db.get_metadata(self.id(index), index_is_id=True) + fmt = self.column_color_map[key] + try: + color = composite_formatter.safe_format(fmt, mi, '', mi) + return QColor(color) + except: + return None + elif self.is_custom_column(key) and \ + self.custom_columns[key]['datatype'] == 'enumeration': + cc = self.custom_columns[self.column_map[col]]['display'] + colors = cc.get('enum_colors', []) + values = cc.get('enum_values', []) + txt = unicode(index.data(Qt.DisplayRole).toString()) + if len(colors) > 0 and txt in values: + try: + return QColor(colors[values.index(txt)]) + except: + pass + return None elif role == Qt.DecorationRole: if self.column_to_dc_decorator_map[col] is not None: return self.column_to_dc_decorator_map[index.column()](index.row()) diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index ee2d7a5428..fc6990fcc9 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -159,6 +159,13 @@ 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) + choices = db.field_metadata.displayable_field_keys() + choices.sort(key=sort_key) + choices.insert(0, '') + for i in range(1, db.column_color_count+1): + r('column_color_name_'+str(i), db.prefs, choices=choices) + r('column_color_template_'+str(i), db.prefs) + def initialize(self): ConfigWidgetBase.initialize(self) font = gprefs['font'] @@ -238,6 +245,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): return rr def refresh_gui(self, gui): + gui.library_view.model().set_color_templates() 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 244b811cbd..d7fca70c08 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -407,6 +407,84 @@ then the tags will be displayed each on their own line.
+ + + + :/images/cover_flow.png:/images/cover_flow.png + + + Column Coloring + + + + + + Column name + + + + + + + Selection template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Qt::Vertical + + + + 690 + 283 + + + + + +
@@ -417,6 +495,11 @@ then the tags will be displayed each on their own line. QLineEdit
calibre/gui2/complete.h
+ + LineEditWithTextBox + QLineEdit +
calibre/gui2/dialogs/template_line_editor.h
+
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 9a740a08b7..819ac2cd24 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -211,6 +211,10 @@ 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)] = '' # Migrate the bool tristate tweak defs['bools_are_tristate'] = \ From 3c92c4a988eca819c813baf2f306cc0cfbff69c9 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 22 May 2011 14:14:23 +0100 Subject: [PATCH 05/14] More work on coloring columns. Refactor the template editor, add some documentation for the new template function first_non_empty, add help text to the configuration dialog. --- .../gui2/dialogs/template_line_editor.py | 2 +- src/calibre/gui2/library/models.py | 2 - src/calibre/gui2/preferences/look_feel.py | 26 +++++++- src/calibre/gui2/preferences/look_feel.ui | 66 +++++++++++-------- src/calibre/gui2/preferences/plugboard.py | 26 +------- src/calibre/manual/template_lang.rst | 1 + src/calibre/utils/formatter_functions.py | 19 +++++- 7 files changed, 85 insertions(+), 57 deletions(-) diff --git a/src/calibre/gui2/dialogs/template_line_editor.py b/src/calibre/gui2/dialogs/template_line_editor.py index d7ba8e4900..69999f59a0 100644 --- a/src/calibre/gui2/dialogs/template_line_editor.py +++ b/src/calibre/gui2/dialogs/template_line_editor.py @@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en' from PyQt4.Qt import (SIGNAL, QLineEdit) from calibre.gui2.dialogs.template_dialog import TemplateDialog -class LineEditWithTextBox(QLineEdit): +class TemplateLineEditor(QLineEdit): ''' Extend the context menu of a QLineEdit to include more actions. diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 7d6cfadacb..83bf5868ba 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -535,12 +535,10 @@ class BooksModel(QAbstractTableModel): # {{{ return img def set_color_templates(self): - print 'here' self.column_color_map = {} for i in range(1,self.db.column_color_count+1): name = self.db.prefs.get('column_color_name_'+str(i)) if name: - print name, self.db.prefs.get('column_color_template_'+str(i)) self.column_color_map[name] = \ self.db.prefs.get('column_color_template_'+str(i)) self.refresh() diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index fc6990fcc9..97400c45bd 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -6,7 +6,7 @@ __copyright__ = '2010, Kovid Goyal ' __docformat__ = 'restructuredtext en' from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog, - QAbstractListModel, Qt) + QAbstractListModel, Qt, QColor) from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList from calibre.gui2.preferences.look_feel_ui import Ui_Form @@ -159,12 +159,36 @@ 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.setWordWrap(True) + self.color_help_text.setText('

' + + _('Here you can specify coloring rules for fields shown in the ' + 'library view. Choose the field you wish to color, then ' + 'supply a template that specifies the color to use.') + + '

' + + _('The template must evaluate to one of the color names shown ' + 'below. 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 blue if the book has the tag "Science ' + 'Fiction", red if the book has the tag "Mystery", or black if ' + 'the book has neither tag, use ' + '"{tags:switch(Science Fiction,blue,Mystery,red,)}" ' + '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')\"") + + '

' + + _('Note: if you want to color a "custom column with a fixed set ' + 'of values", it is possible and often easier to specify the ' + 'colors in the column definition dialog. There you can ' + 'provide a color for each value without using a template.')+ '

') choices = db.field_metadata.displayable_field_keys() choices.sort(key=sort_key) choices.insert(0, '') for i in range(1, db.column_color_count+1): r('column_color_name_'+str(i), db.prefs, choices=choices) r('column_color_template_'+str(i), db.prefs) + all_colors = [unicode(s) for s in list(QColor.colorNames())] + self.colors_box.setText(', '.join(all_colors)) def initialize(self): ConfigWidgetBase.initialize(self) diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index d7fca70c08..aa5afe26dd 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -416,72 +416,80 @@ then the tags will be displayed each on their own line. Column Coloring - + Column name - + + + + + Selection template - + - - - - - - - - - + - + - + - + - + - + - + - - - - Qt::Vertical + + + + + + + + + + + + Color names - - - 690 - 283 - + + + + + + + 0 + 1 + - + @@ -496,7 +504,7 @@ then the tags will be displayed each on their own line.
calibre/gui2/complete.h
- LineEditWithTextBox + TemplateLineEditor QLineEdit
calibre/gui2/dialogs/template_line_editor.h
diff --git a/src/calibre/gui2/preferences/plugboard.py b/src/calibre/gui2/preferences/plugboard.py index b4b1d4e08e..cf632c04c0 100644 --- a/src/calibre/gui2/preferences/plugboard.py +++ b/src/calibre/gui2/preferences/plugboard.py @@ -7,12 +7,12 @@ __docformat__ = 'restructuredtext en' import copy -from PyQt4.Qt import Qt, QLineEdit, QComboBox, SIGNAL, QListWidgetItem +from PyQt4.Qt import Qt, QComboBox, QListWidgetItem from calibre.customize.ui import is_disabled from calibre.gui2 import error_dialog, question_dialog from calibre.gui2.device import device_name_for_plugboards -from calibre.gui2.dialogs.template_dialog import TemplateDialog +from calibre.gui2.dialogs.template_line_editor import TemplateLineEditor from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences.plugboard_ui import Ui_Form from calibre.customize.ui import metadata_writers, device_plugins @@ -24,26 +24,6 @@ from calibre.library.server.content import plugboard_content_server_value, \ from calibre.utils.formatter import validation_formatter -class LineEditWithTextBox(QLineEdit): - - ''' - Extend the context menu of a QLineEdit to include more actions. - ''' - - def contextMenuEvent(self, event): - menu = self.createStandardContextMenu() - menu.addSeparator() - - action_open_editor = menu.addAction(_('Open Editor')) - - self.connect(action_open_editor, SIGNAL('triggered()'), self.open_editor) - menu.exec_(event.globalPos()) - - def open_editor(self): - t = TemplateDialog(self, self.text()) - if t.exec_(): - self.setText(t.textbox.toPlainText()) - class ConfigWidget(ConfigWidgetBase, Ui_Form): def genesis(self, gui): @@ -107,7 +87,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): self.source_widgets = [] self.dest_widgets = [] for i in range(0, len(self.dest_fields)-1): - w = LineEditWithTextBox(self) + w = TemplateLineEditor(self) self.source_widgets.append(w) self.fields_layout.addWidget(w, 5+i, 0, 1, 1) w = QComboBox(self) diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index 0fd396fb64..9b5fe63f25 100644 --- a/src/calibre/manual/template_lang.rst +++ b/src/calibre/manual/template_lang.rst @@ -234,6 +234,7 @@ The following functions are available in addition to those described in single-f * ``cmp(x, y, lt, eq, gt)`` -- compares x and y after converting both to numbers. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``. * ``divide(x, y)`` -- returns x / y. Throws an exception if either x or y are not numbers. * ``field(name)`` -- returns the metadata field named by ``name``. + * ``first_non_empty(value, value, ...) -- returns the first value that is not empty. If all values are empty, then the empty value is returned. You can have as many values as you want. * ``format_date(x, date_format)`` -- format_date(val, format_string) -- format the value, which must be a date field, using the format_string, returning a string. The formatting codes are:: d : the day as number without a leading zero (1 to 31) diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index aa8e4fb3a3..59a750bcc5 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -562,6 +562,22 @@ class BuiltinBooksize(BuiltinFormatterFunction): pass return '' +class BuiltinFirstNonEmpty(BuiltinFormatterFunction): + name = 'first_non_empty' + arg_count = -1 + doc = _('first_non_empty(value, value, ...) -- ' + 'returns the first value that is not empty. If all values are ' + 'empty, then the empty value is returned.' + 'You can have as many values as you want.') + + def evaluate(self, formatter, kwargs, mi, locals, *args): + i = 0 + while i < len(args): + if args[i]: + return args[i] + i += 1 + return '' + builtin_add = BuiltinAdd() builtin_assign = BuiltinAssign() builtin_booksize = BuiltinBooksize() @@ -571,8 +587,9 @@ builtin_contains = BuiltinContains() builtin_count = BuiltinCount() builtin_divide = BuiltinDivide() builtin_eval = BuiltinEval() -builtin_format_date = BuiltinFormat_date() +builtin_first_non_empty = BuiltinFirstNonEmpty() builtin_field = BuiltinField() +builtin_format_date = BuiltinFormat_date() builtin_ifempty = BuiltinIfempty() builtin_list_item = BuiltinListitem() builtin_lookup = BuiltinLookup() From 4ddb1e852ba181d6cfd03dcb8bd31aae758475d1 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 22 May 2011 15:22:33 +0100 Subject: [PATCH 06/14] Improvements on coloring: add an in_list formatter function. Add more documentation in the preference screen. Add a scroll bar to the doc in the preferences screen. --- src/calibre/gui2/preferences/look_feel.py | 24 ++++++++++++++++------- src/calibre/gui2/preferences/look_feel.ui | 10 +++++++++- src/calibre/utils/formatter_functions.py | 17 ++++++++++++++++ 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index 97400c45bd..c96d980505 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -159,7 +159,6 @@ 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.setWordWrap(True) self.color_help_text.setText('

' + _('Here you can specify coloring rules for fields shown in the ' 'library view. Choose the field you wish to color, then ' @@ -169,14 +168,25 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): 'below. 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 blue if the book has the tag "Science ' - 'Fiction", red if the book has the tag "Mystery", or black if ' - 'the book has neither tag, use ' - '"{tags:switch(Science Fiction,blue,Mystery,red,)}" ' + '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')\"") + + '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 possible and often easier to specify the ' 'colors in the column definition dialog. There you can ' diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index aa5afe26dd..1194109c6c 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -424,7 +424,12 @@ then the tags will be displayed each on their own line. - + + + true + + + @@ -483,6 +488,9 @@ then the tags will be displayed each on their own line. + + true + 0 diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py index 59a750bcc5..c53277f3ce 100644 --- a/src/calibre/utils/formatter_functions.py +++ b/src/calibre/utils/formatter_functions.py @@ -327,6 +327,22 @@ class BuiltinSwitch(BuiltinFormatterFunction): return args[i+1] i += 2 +class BuiltinInList(BuiltinFormatterFunction): + name = 'in_list' + arg_count = 5 + doc = _('in_list(val, separator, pattern, found_val, not_found_val) -- ' + 'treat val as a list of items separated by separator, ' + 'comparing the pattern against each value in the list. If the ' + 'pattern matches a value, return found_val, otherwise return ' + 'not_found_val.') + + def evaluate(self, formatter, kwargs, mi, locals, val, sep, pat, fv, nfv): + l = [v.strip() for v in val.split(sep) if v.strip()] + for v in l: + if re.search(pat, v): + return fv + return nfv + class BuiltinRe(BuiltinFormatterFunction): name = 're' arg_count = 3 @@ -591,6 +607,7 @@ builtin_first_non_empty = BuiltinFirstNonEmpty() builtin_field = BuiltinField() builtin_format_date = BuiltinFormat_date() builtin_ifempty = BuiltinIfempty() +builtin_in_list = BuiltinInList() builtin_list_item = BuiltinListitem() builtin_lookup = BuiltinLookup() builtin_lowercase = BuiltinLowercase() From ca5dc817c22e70ada00dd0c9feb7ad8ea0ea0238 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 22 May 2011 15:26:32 +0100 Subject: [PATCH 07/14] Add the new in_list function to the documentation --- src/calibre/manual/template_lang.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index 9b5fe63f25..69c77e5bfd 100644 --- a/src/calibre/manual/template_lang.rst +++ b/src/calibre/manual/template_lang.rst @@ -123,7 +123,8 @@ The functions available are: * ``contains(pattern, text if match, text if not match`` -- checks if field contains matches for the regular expression `pattern`. Returns `text if match` if matches are found, otherwise it returns `text if no match`. * ``count(separator)`` -- interprets the value as a list of items separated by `separator`, returning the number of items in the list. Most lists use a comma as the separator, but authors uses an ampersand. Examples: `{tags:count(,)}`, `{authors:count(&)}` * ``ifempty(text)`` -- if the field is not empty, return the value of the field. Otherwise return `text`. - * ``list_item(index, separator)`` -- interpret the value as a list of items separated by `separator`, returning the `index`th item. The first item is number zero. The last item can be returned using `list_item(-1,separator)`. If the item is not in the list, then the empty value is returned. The separator has the same meaning as in the `count` function. + * ``in_list(separator, pattern, found_val, not_found_val)`` -- interpret the field as a list of items separated by `separator`, comparing the `pattern` against each value in the list. If the pattern matches a value, return `found_val`, otherwise return `not_found_val`. + * ``list_item(index, separator)`` -- interpret the field as a list of items separated by `separator`, returning the `index`th item. The first item is number zero. The last item can be returned using `list_item(-1,separator)`. If the item is not in the list, then the empty value is returned. The separator has the same meaning as in the `count` function. * ``re(pattern, replacement)`` -- return the field after applying the regular expression. All instances of `pattern` are replaced with `replacement`. As in all of |app|, these are python-compatible regular expressions. * ``shorten(left chars, middle text, right chars)`` -- Return a shortened version of the field, consisting of `left chars` characters from the beginning of the field, followed by `middle text`, followed by `right chars` characters from the end of the string. `Left chars` and `right chars` must be integers. For example, assume the title of the book is `Ancient English Laws in the Times of Ivanhoe`, and you want it to fit in a space of at most 15 characters. If you use ``{title:shorten(9,-,5)}``, the result will be `Ancient E-nhoe`. If the field's length is less than ``left chars`` + ``right chars`` + the length of ``middle text``, then the field will be used intact. For example, the title `The Dome` would not be changed. * ``switch(pattern, value, pattern, value, ..., else_value)`` -- for each ``pattern, value`` pair, checks if the field matches the regular expression ``pattern`` and if so, returns that ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs as you want. From 1a4768a539bfdf65b2f5a6d68a133730683f9261 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 22 May 2011 18:32:03 +0100 Subject: [PATCH 08/14] Make coloring work across change_libraries --- src/calibre/gui2/init.py | 1 - src/calibre/gui2/library/models.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/init.py b/src/calibre/gui2/init.py index 079e1814c3..a75ff01b21 100644 --- a/src/calibre/gui2/init.py +++ b/src/calibre/gui2/init.py @@ -65,7 +65,6 @@ class LibraryViewMixin(object): # {{{ self.build_context_menus() self.library_view.model().set_highlight_only(config['highlight_search_matches']) - self.library_view.model().set_color_templates() def build_context_menus(self): lm = QMenu(self) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 83bf5868ba..a3e7438908 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -157,6 +157,7 @@ class BooksModel(QAbstractTableModel): # {{{ self.database_changed.emit(db) self.stop_metadata_backup() self.start_metadata_backup() + self.set_color_templates() def start_metadata_backup(self): self.metadata_backup = MetadataBackup(self.db) @@ -535,6 +536,7 @@ class BooksModel(QAbstractTableModel): # {{{ return img def set_color_templates(self): + print 'here' self.column_color_map = {} for i in range(1,self.db.column_color_count+1): name = self.db.prefs.get('column_color_name_'+str(i)) From 6087a6a6603868fa752b15a30b074ebd7e7e7243 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 22 May 2011 18:34:34 +0100 Subject: [PATCH 09/14] Remove print statement --- src/calibre/gui2/library/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index a3e7438908..b378256a42 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -536,7 +536,6 @@ class BooksModel(QAbstractTableModel): # {{{ return img def set_color_templates(self): - print 'here' self.column_color_map = {} for i in range(1,self.db.column_color_count+1): name = self.db.prefs.get('column_color_name_'+str(i)) From 3487ed762ea084d6d44d9e3a9446f6e87e87e1a8 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 22 May 2011 19:34:03 +0100 Subject: [PATCH 10/14] Switch refresh to reset when loading column color templates --- src/calibre/gui2/library/models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index b378256a42..2576518d92 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -153,11 +153,11 @@ 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() self.start_metadata_backup() - self.set_color_templates() def start_metadata_backup(self): self.metadata_backup = MetadataBackup(self.db) @@ -535,14 +535,15 @@ class BooksModel(QAbstractTableModel): # {{{ img = self.default_image return img - def set_color_templates(self): + def set_color_templates(self, reset=True): self.column_color_map = {} 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_map[name] = \ self.db.prefs.get('column_color_template_'+str(i)) - self.refresh() + if reset: + self.reset() def build_data_convertors(self): def authors(r, idx=-1): From 0bd580f572cd5b5a10c9167adc6f8f5c717d0dc3 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 22 May 2011 19:53:26 +0100 Subject: [PATCH 11/14] Remove unused PEN declaration --- src/calibre/gui2/library/delegates.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index 6d962c9129..4f002c2c48 100644 --- a/src/calibre/gui2/library/delegates.py +++ b/src/calibre/gui2/library/delegates.py @@ -28,7 +28,6 @@ from calibre.gui2.dialogs.template_dialog import TemplateDialog class RatingDelegate(QStyledItemDelegate): # {{{ COLOR = QColor("blue") SIZE = 16 - PEN = QPen(COLOR, 1, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin) def __init__(self, parent): QStyledItemDelegate.__init__(self, parent) From 29b453ba23177adc9cd740cada0f6b7ba08e23f4 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 22 May 2011 19:55:46 +0100 Subject: [PATCH 12/14] Check for color valid when using column coloring --- src/calibre/gui2/library/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 2576518d92..6e8e79d3b3 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -726,7 +726,9 @@ class BooksModel(QAbstractTableModel): # {{{ txt = unicode(index.data(Qt.DisplayRole).toString()) if len(colors) > 0 and txt in values: try: - return QColor(colors[values.index(txt)]) + color = colors[values.index(txt)] + if QColor.isValid(color): + return QColor(color) except: pass return None From de969af505aeaf32507c19229081371a850549b9 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 22 May 2011 20:12:47 +0100 Subject: [PATCH 13/14] Improve robustness in column coloring. --- src/calibre/gui2/library/models.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 6e8e79d3b3..9d90c44f18 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -715,7 +715,8 @@ class BooksModel(QAbstractTableModel): # {{{ fmt = self.column_color_map[key] try: color = composite_formatter.safe_format(fmt, mi, '', mi) - return QColor(color) + if QColor.isValid(color): + return QColor(color) except: return None elif self.is_custom_column(key) and \ From 478369c7cba88cf04d8778d8d4b2ea14c40872ed Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Sun, 22 May 2011 20:47:47 +0100 Subject: [PATCH 14/14] Add colors icon to preferences dialog. Correctly clean template values when the column is set to empty. --- src/calibre/gui2/preferences/look_feel.py | 8 +++++++- src/calibre/gui2/preferences/look_feel.ui | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index c96d980505..ffbc82eefd 100644 --- a/src/calibre/gui2/preferences/look_feel.py +++ b/src/calibre/gui2/preferences/look_feel.py @@ -194,7 +194,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): choices = db.field_metadata.displayable_field_keys() choices.sort(key=sort_key) choices.insert(0, '') - for i in range(1, db.column_color_count+1): + self.column_color_count = db.column_color_count+1 + for i in range(1, self.column_color_count): r('column_color_name_'+str(i), db.prefs, choices=choices) r('column_color_template_'+str(i), db.prefs) all_colors = [unicode(s) for s in list(QColor.colorNames())] @@ -267,6 +268,11 @@ 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)) + if not col.currentText(): + temp = getattr(self, 'opt_column_color_template_'+str(i)) + temp.setText('') rr = ConfigWidgetBase.commit(self, *args) if self.current_font != self.initial_font: gprefs['font'] = (self.current_font[:4] if self.current_font else diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui index 1194109c6c..9dedcf4f8c 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -410,7 +410,7 @@ then the tags will be displayed each on their own line. - :/images/cover_flow.png:/images/cover_flow.png + :/images/format-fill-color.png:/images/format-fill-color.png Column Coloring