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..c3598a8abb --- /dev/null +++ b/src/calibre/gui2/dialogs/template_line_editor.py @@ -0,0 +1,32 @@ +#!/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 QLineEdit +from calibre.gui2.dialogs.template_dialog import TemplateDialog + +class TemplateLineEditor(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 Template Editor')) + + action_open_editor.triggered.connect(self.open_editor) + menu.exec_(event.globalPos()) + + def open_editor(self): + t = TemplateDialog(self, self.text()) + t.setWindowTitle(_('Edit template')) + if t.exec_(): + self.setText(t.textbox.toPlainText()) + + diff --git a/src/calibre/gui2/library/delegates.py b/src/calibre/gui2/library/delegates.py index 7265be89c8..50c411aaa4 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, +from PyQt4.Qt import (QColor, Qt, QModelIndex, QSize, QApplication, QPainterPath, QLinearGradient, QBrush, 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 @@ -62,12 +63,14 @@ 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: painter.fillRect(option.rect, option.palette.highlight()) + else: + painter.fillRect(option.rect, option.backgroundBrush) + try: painter.setRenderHint(QPainter.Antialiasing) painter.setClipRect(option.rect) @@ -316,18 +319,22 @@ class CcCommentsDelegate(QStyledItemDelegate): # {{{ self.document = QTextDocument() def paint(self, painter, option, index): - style = self.parent().style() - self.document.setHtml(index.data(Qt.DisplayRole).toString()) - painter.save() + self.initStyleOption(option, index) + style = QApplication.style() if option.widget is None \ + else option.widget.style() + self.document.setHtml(option.text) + option.text = u'' 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.restore() + style.drawControl(QStyle.CE_ItemViewItem, option, painter) + ctx = QAbstractTextDocumentLayout.PaintContext() + ctx.palette = option.palette #.setColor(QPalette.Text, QColor("red")); + if hasattr(QStyle, 'SE_ItemViewItemText'): + textRect = style.subElementRect(QStyle.SE_ItemViewItemText, option) + painter.save() + painter.translate(textRect.topLeft()) + painter.setClipRect(textRect.translated(-textRect.topLeft())) + self.document.documentLayout().draw(painter, ctx) + painter.restore() def createEditor(self, parent, option, index): m = index.model() diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index 0baf98ecdd..d698655746 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): @@ -151,6 +153,7 @@ 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() @@ -532,6 +535,15 @@ class BooksModel(QAbstractTableModel): # {{{ img = self.default_image return img + 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)) + if reset: + self.reset() def build_data_convertors(self): def authors(r, idx=-1): @@ -696,6 +708,31 @@ class BooksModel(QAbstractTableModel): # {{{ elif role == Qt.BackgroundRole: if self.id(index) in self.ids_to_highlight_set: return QVariant(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 = QColor(composite_formatter.safe_format(fmt, mi, '', mi)) + if color.isValid(): + return QVariant(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: + color = QColor(colors[values.index(txt)]) + if color.isValid(): + return QVariant(color) + 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/create_custom_column.py b/src/calibre/gui2/preferences/create_custom_column.py index 7b891b782c..3a245580dd 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,11 +126,15 @@ 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)) 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): @@ -170,7 +174,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 +251,20 @@ 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 = [] + 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 diff --git a/src/calibre/gui2/preferences/look_feel.py b/src/calibre/gui2/preferences/look_feel.py index ee2d7a5428..1c9d4abfce 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,6 +159,51 @@ 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.') + + '

' + + _('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 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.')+ '

') + choices = db.field_metadata.displayable_field_keys() + choices.sort(key=sort_key) + choices.insert(0, '') + 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())] + self.colors_box.setText(', '.join(all_colors)) + def initialize(self): ConfigWidgetBase.initialize(self) font = gprefs['font'] @@ -226,6 +271,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 @@ -238,6 +288,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..970052ad2e 100644 --- a/src/calibre/gui2/preferences/look_feel.ui +++ b/src/calibre/gui2/preferences/look_feel.ui @@ -7,7 +7,7 @@ 0 0 717 - 390 + 519 @@ -407,6 +407,125 @@ then the tags will be displayed each on their own line.
+ + + + :/images/format-fill-color.png:/images/format-fill-color.png + + + Column Coloring + + + + + + Column name + + + + + + + Selection template + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Color names + + + + + + + + 0 + 1 + + + + + 16777215 + 100 + + + + true + + + + + + + + 0 + 200 + + + + true + + + Qt::AlignCenter + + + + + 0 + 0 + 687 + 194 + + + + + + + true + + + true + + + + + + + + +
@@ -417,6 +536,11 @@ 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/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/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'] = \ diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst index 0fd396fb64..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. @@ -234,6 +235,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..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 @@ -562,6 +578,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,9 +603,11 @@ 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_in_list = BuiltinInList() builtin_list_item = BuiltinListitem() builtin_lookup = BuiltinLookup() builtin_lowercase = BuiltinLowercase()