New preferences interface for column coloring. Note that editing of advanced rules is not yet implemented.

This commit is contained in:
Kovid Goyal 2011-06-01 21:47:03 -06:00
parent f6f8963653
commit 6b8a1442c1
6 changed files with 79 additions and 312 deletions

View File

@ -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']

View File

@ -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,19 +507,20 @@ 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:
td = TemplateDialog(self, rule, None)
if td.exec_() == td.Accepted:
self.changed.emit()
pass # TODO
def add_advanced(self):
pass
def get_selected_row(self, txt):
sm = self.rules_view.selectionModel()
rows = list(sm.selectedRows())
@ -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)

View File

@ -5,21 +5,19 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__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('<p>' +
_('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 '
'<a href="http://manual.calibre-ebook.com/template_lang.html">'
'tutorial</a> on using templates.') +
'</p><p>' +
_('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.') +
'</p><p>' +
_('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 '
'<pre>{#column:switch(foo,blue,bar,red,black)}</pre>'
'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'
"<pre>program: \n"
" t = field('tags'); \n"
" first_non_empty(\n"
" in_list(t, ',', '^Science Fiction$', 'blue', ''), \n"
" in_list(t, ',', '^Mystery$', 'red', 'black'))</pre>"
'To show the title in green if it has one format, blue if it '
'two formats, and red if more, use'
"<pre>program:cmp(count(field('formats'),','), 2, 'green', 'blue', 'red')</pre>") +
'</p><p>' +
_('You can access a multi-line template editor from the '
'context menu (right-click).') + '</p><p>' +
_('<b>Note:</b> 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.')+ '</p>')
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()

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>717</width>
<width>820</width>
<height>519</height>
</rect>
</property>
@ -407,161 +407,6 @@ then the tags will be displayed each on their own line.</string>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_5">
<attribute name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/format-fill-color.png</normaloff>:/images/format-fill-color.png</iconset>
</attribute>
<attribute name="title">
<string>Column Coloring</string>
</attribute>
<layout class="QGridLayout" name="column_color_layout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Column to color</string>
</property>
</widget>
</item>
<item row="0" column="3">
<layout class="QHBoxLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Color selection template</string>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
<horstretch>10</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>The template wizard is easiest to use</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="color_help_button">
<property name="text">
<string>Show/hide help text</string>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="colors_button">
<property name="text">
<string>Show/hide colors</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="20" column="0">
<widget class="QLabel" name="colors_label">
<property name="text">
<string>Color names</string>
</property>
</widget>
</item>
<item row="21" column="0" colspan="8">
<widget class="QScrollArea" name="colors_scrollArea">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>300</height>
</size>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents_2">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>687</width>
<height>61</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="colors_box">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignLeft|Qt::AlignTop</set>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="30" column="0" colspan="8">
<widget class="QScrollArea" name="color_help_scrollArea">
<property name="minimumSize">
<size>
<width>0</width>
<height>200</height>
</size>
</property>
<property name="widgetResizable">
<bool>true</bool>
</property>
<property name="alignment">
<set>Qt::AlignLeft|Qt::AlignTop</set>
</property>
<widget class="QWidget" name="scrollAreaWidgetContents">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>687</width>
<height>194</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="color_help_text">
<property name="wordWrap">
<bool>true</bool>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
</widget>
</item>
<item row="40" column="0">
<spacer>
<property name="sizePolicy">
<sizepolicy vsizetype="Expanding" hsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>10</verstretch>
</sizepolicy>
</property>
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>0</width>
<height>0</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</widget>
</item>
</layout>
@ -572,11 +417,6 @@ then the tags will be displayed each on their own line.</string>
<extends>QLineEdit</extends>
<header>calibre/gui2/complete.h</header>
</customwidget>
<customwidget>
<class>TemplateLineEditor</class>
<extends>QLineEdit</extends>
<header>calibre/gui2/dialogs/template_line_editor.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../../../resources/images.qrc"/>

View File

@ -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

View File

@ -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]